Repository: microsoft/vscode-copilot-chat Branch: main Commit: 20266e339733 Files: 4307 Total size: 43.9 MB Directory structure: gitextract_87srrczz/ ├── .agents/ │ └── skills/ │ └── launch/ │ └── SKILL.md ├── .claude/ │ └── agents/ │ └── anthropic-sdk-upgrader.md ├── .devcontainer/ │ ├── devcontainer-lock.json │ └── devcontainer.json ├── .esbuild.ts ├── .eslint-ignore ├── .eslintplugin/ │ ├── index.ts │ ├── no-bad-gdpr-comment.ts │ ├── no-funny-filename.ts │ ├── no-gdpr-event-name-mismatch.ts │ ├── no-instanceof-uri.ts │ ├── no-missing-linebreak.ts │ ├── no-nls-localize.ts │ ├── no-restricted-copilot-pr-string.ts │ ├── no-runtime-import.ts │ ├── no-test-imports.ts │ ├── no-test-only.ts │ ├── no-unexternalized-strings.ts │ ├── no-unlayered-files.ts │ ├── package.json │ ├── tsconfig.json │ └── utils.ts ├── .gitattributes ├── .github/ │ ├── CODENOTIFY │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ └── config.yml │ ├── commands.json │ ├── copilot-instructions.md │ ├── dependabot.yml │ ├── instructions/ │ │ ├── prompt-tsx.instructions.md │ │ └── vitest-unit-tests.instructions.md │ ├── prompts/ │ │ ├── updateCopilotCLIToolMapping.prompt.md │ │ └── updateGithubCopilotSDK.prompt.md │ └── workflows/ │ ├── copilot-setup-steps.yml │ ├── ensure-node-modules-cache.yml │ ├── npm-package.yml │ └── pr.yml ├── .gitignore ├── .husky/ │ ├── pre-commit │ └── pre-push ├── .mocha-multi-reporters.js ├── .mocharc.js ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .vscode/ │ ├── conversation.schema.json │ ├── extensions/ │ │ ├── test-extension/ │ │ │ ├── .vscode/ │ │ │ │ └── launch.json │ │ │ ├── bootstrap.ts │ │ │ ├── main.ts │ │ │ └── package.json │ │ └── visualization-runner/ │ │ ├── README.md │ │ ├── entry.js │ │ ├── extension.ts │ │ └── package.json │ ├── extensions.json │ ├── launch.json │ ├── mcp.json │ ├── settings.json │ ├── snippets.code-snippets │ ├── state.schema.json │ └── tasks.json ├── .vscode-test.mjs ├── .vscodeignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CodeQL.yml ├── LICENSE.txt ├── README.md ├── SECURITY.md ├── assets/ │ └── prompts/ │ ├── create-agent.prompt.md │ ├── create-hook.prompt.md │ ├── create-instructions.prompt.md │ ├── create-prompt.prompt.md │ ├── create-skill.prompt.md │ ├── init.prompt.md │ ├── plan.prompt.md │ └── skills/ │ ├── agent-customization/ │ │ ├── SKILL.md │ │ └── references/ │ │ ├── agents.md │ │ ├── hooks.md │ │ ├── instructions.md │ │ ├── prompts.md │ │ ├── skills.md │ │ └── workspace-instructions.md │ ├── get-search-view-results/ │ │ └── SKILL.md │ ├── install-vscode-extension/ │ │ └── SKILL.md │ ├── project-setup-info-context7/ │ │ └── SKILL.md │ ├── project-setup-info-local/ │ │ └── SKILL.md │ └── troubleshoot/ │ └── SKILL.md ├── build/ │ ├── .cachesalt │ ├── listBuildCacheFiles.js │ ├── npm-package.yml │ ├── pr-check-cache-files.ts │ ├── pre-release.yml │ ├── release.yml │ ├── setup-emsdk.sh │ └── update-assets.yml ├── cgmanifest.json ├── chat-lib/ │ ├── .gitignore │ ├── LICENSE.txt │ ├── README.md │ ├── package.json │ ├── script/ │ │ └── postinstall.ts │ ├── tsconfig.base.json │ ├── tsconfig.json │ └── vitest.config.ts ├── docs/ │ ├── NES_EXPECTED_EDIT_CAPTURE.md │ ├── monitoring/ │ │ ├── agent_monitoring.md │ │ ├── agent_monitoring_arch.md │ │ ├── docker-compose.yaml │ │ ├── otel-collector-config.yaml │ │ └── otel-data-flow.html │ ├── prompts.md │ └── tools.md ├── eslint.config.mjs ├── lint-staged.config.js ├── package.json ├── package.nls.json ├── script/ │ ├── alternativeAction/ │ │ ├── index.ts │ │ ├── processor.ts │ │ ├── types.ts │ │ └── util.ts │ ├── analyzeEdits.ts │ ├── applyLocalDts.sh │ ├── build/ │ │ ├── compressTikToken.ts │ │ ├── copyStaticAssets.ts │ │ ├── downloadBinary.ts │ │ ├── extractChatLib.ts │ │ ├── moveProposedDts.js │ │ ├── vscodeDtsCheck.js │ │ └── vscodeDtsUpdate.js │ ├── cleanLog.ts │ ├── compareStestAlternativeRuns.ts │ ├── electron/ │ │ ├── simulationWorkbench.css │ │ ├── simulationWorkbench.html │ │ └── simulationWorkbenchMain.js │ ├── eslintGitBlameReport/ │ │ └── generateEslintIgnoreReport.ts │ ├── logRecordingTypes.ts │ ├── postinstall.ts │ ├── scoredEditsReconciler.ts │ ├── setup/ │ │ ├── copySources.ts │ │ ├── createVenv.mts │ │ ├── getEnv.mts │ │ └── getToken.mts │ ├── simulate.ps1 │ ├── simulate.sh │ ├── test/ │ │ └── scoredEditsReconciler.spec.ts │ ├── testGeneration/ │ │ └── editFromPatchTests.ts │ └── tsconfig.json ├── src/ │ ├── extension/ │ │ ├── agentDebug/ │ │ │ ├── common/ │ │ │ │ └── toolResultRenderer.ts │ │ │ └── vscode-node/ │ │ │ └── toolResultContentRenderer.ts │ │ ├── agents/ │ │ │ ├── node/ │ │ │ │ ├── adapters/ │ │ │ │ │ ├── anthropicAdapter.ts │ │ │ │ │ ├── openaiAdapterForSTests.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── langModelServer.ts │ │ │ │ └── test/ │ │ │ │ ├── mockLanguageModelServer.ts │ │ │ │ └── openaiAdapter.spec.ts │ │ │ └── vscode-node/ │ │ │ ├── agentCustomizationSkillProvider.ts │ │ │ ├── agentTypes.ts │ │ │ ├── askAgentProvider.ts │ │ │ ├── baseSkillProvider.ts │ │ │ ├── editModeAgentProvider.ts │ │ │ ├── exploreAgentProvider.ts │ │ │ ├── githubOrgChatResourcesService.ts │ │ │ ├── githubOrgCustomAgentProvider.ts │ │ │ ├── githubOrgInstructionsProvider.ts │ │ │ ├── planAgentProvider.ts │ │ │ ├── promptFileContrib.ts │ │ │ ├── skillFsProviderHelper.ts │ │ │ ├── test/ │ │ │ │ ├── askAgentProvider.spec.ts │ │ │ │ ├── githubOrgChatResourcesService.spec.ts │ │ │ │ ├── githubOrgCustomAgentProvider.spec.ts │ │ │ │ ├── githubOrgInstructionsProvider.spec.ts │ │ │ │ ├── mockOctoKitService.ts │ │ │ │ └── planAgentProvider.spec.ts │ │ │ └── troubleshootSkillProvider.ts │ │ ├── api/ │ │ │ └── vscode/ │ │ │ ├── api.d.ts │ │ │ ├── extensionApi.ts │ │ │ └── vscodeContextProviderApi.ts │ │ ├── authentication/ │ │ │ └── vscode-node/ │ │ │ └── authentication.contribution.ts │ │ ├── byok/ │ │ │ ├── common/ │ │ │ │ ├── anthropicMessageConverter.ts │ │ │ │ ├── byokProvider.ts │ │ │ │ ├── geminiFunctionDeclarationConverter.ts │ │ │ │ ├── geminiMessageConverter.ts │ │ │ │ └── test/ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ └── anthropicMessageConverter.spec.ts.snap │ │ │ │ ├── anthropicMessageConverter.spec.ts │ │ │ │ ├── geminiFunctionDeclarationConverter.spec.ts │ │ │ │ └── geminiMessageConverter.spec.ts │ │ │ ├── node/ │ │ │ │ ├── azureOpenAIEndpoint.ts │ │ │ │ ├── openAIEndpoint.ts │ │ │ │ └── test/ │ │ │ │ ├── azureOpenAIEndpoint.spec.ts │ │ │ │ └── openAIEndpoint.spec.ts │ │ │ └── vscode-node/ │ │ │ ├── abstractLanguageModelChatProvider.ts │ │ │ ├── anthropicProvider.ts │ │ │ ├── azureProvider.ts │ │ │ ├── byokContribution.ts │ │ │ ├── byokStorageService.ts │ │ │ ├── customOAIProvider.ts │ │ │ ├── geminiNativeProvider.ts │ │ │ ├── ollamaProvider.ts │ │ │ ├── openAIProvider.ts │ │ │ ├── openRouterProvider.ts │ │ │ ├── test/ │ │ │ │ ├── azureProvider.spec.ts │ │ │ │ ├── geminiNativeProvider.spec.ts │ │ │ │ └── ollamaProvider.spec.ts │ │ │ └── xAIProvider.ts │ │ ├── chat/ │ │ │ ├── test/ │ │ │ │ └── node/ │ │ │ │ └── chatHookService.spec.ts │ │ │ └── vscode-node/ │ │ │ ├── chatDebugFileLoggerService.ts │ │ │ ├── chatHookService.ts │ │ │ ├── chatHookTelemetry.ts │ │ │ ├── chatQuota.contribution.ts │ │ │ ├── hooksOutputChannel.ts │ │ │ ├── sessionTranscriptService.ts │ │ │ └── test/ │ │ │ └── chatDebugFileLoggerService.spec.ts │ │ ├── chatSessionContext/ │ │ │ └── vscode-node/ │ │ │ └── chatSessionContextProvider.ts │ │ ├── chatSessions/ │ │ │ ├── claude/ │ │ │ │ ├── AGENTS.md │ │ │ │ ├── CLAUDE_SESSION_USER_GUIDE.md │ │ │ │ ├── common/ │ │ │ │ │ ├── claudeFolderInfo.ts │ │ │ │ │ ├── claudeHookRegistry.ts │ │ │ │ │ ├── claudeMcpServerRegistry.ts │ │ │ │ │ ├── claudeSessionUri.ts │ │ │ │ │ ├── claudeToolPermission.ts │ │ │ │ │ ├── claudeToolPermissionRegistry.ts │ │ │ │ │ ├── claudeToolPermissionService.ts │ │ │ │ │ ├── claudeTools.ts │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── mcpServers/ │ │ │ │ │ │ ├── ideMcpServer.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── slashCommands/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── test/ │ │ │ │ │ │ ├── claudeToolPermissionRegistry.spec.ts │ │ │ │ │ │ ├── ideMcpServer.spec.ts │ │ │ │ │ │ └── toolInvocationFormatter.spec.ts │ │ │ │ │ ├── toolInvocationFormatter.ts │ │ │ │ │ └── toolPermissionHandlers/ │ │ │ │ │ ├── askUserQuestionHandler.ts │ │ │ │ │ ├── bashToolHandler.ts │ │ │ │ │ ├── exitPlanModeHandler.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── node/ │ │ │ │ │ ├── claudeCodeAgent.ts │ │ │ │ │ ├── claudeCodeModels.ts │ │ │ │ │ ├── claudeCodeSdkService.ts │ │ │ │ │ ├── claudeLanguageModelServer.ts │ │ │ │ │ ├── claudeProjectFolders.ts │ │ │ │ │ ├── claudePromptResolver.ts │ │ │ │ │ ├── claudeSessionStateService.ts │ │ │ │ │ ├── claudeSettingsChangeTracker.ts │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── loggingHooks.ts │ │ │ │ │ │ ├── sessionHooks.ts │ │ │ │ │ │ ├── subagentHooks.ts │ │ │ │ │ │ └── toolHooks.ts │ │ │ │ │ ├── mcpServers/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── sessionParser/ │ │ │ │ │ │ ├── README.md │ │ │ │ │ │ ├── claudeCodeSessionService.ts │ │ │ │ │ │ ├── claudeSessionParser.ts │ │ │ │ │ │ ├── claudeSessionSchema.ts │ │ │ │ │ │ ├── sdkSessionAdapter.ts │ │ │ │ │ │ └── test/ │ │ │ │ │ │ ├── claudeCodeSessionService.spec.ts │ │ │ │ │ │ ├── claudeSessionParser.spec.ts │ │ │ │ │ │ ├── claudeSessionSchema.spec.ts │ │ │ │ │ │ └── sdkSessionAdapter.spec.ts │ │ │ │ │ ├── slashCommands/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── test/ │ │ │ │ │ │ ├── askUserQuestionHandler.spec.ts │ │ │ │ │ │ ├── claudeCodeAgent.spec.ts │ │ │ │ │ │ ├── claudeCodeAgentOTel.spec.ts │ │ │ │ │ │ ├── claudeCodeModels.spec.ts │ │ │ │ │ │ ├── claudeProjectFolders.spec.ts │ │ │ │ │ │ ├── claudeSessionStateService.spec.ts │ │ │ │ │ │ ├── claudeSettingsChangeTracker.spec.ts │ │ │ │ │ │ ├── claudeToolPermissionService.spec.ts │ │ │ │ │ │ ├── extractSessionId.spec.ts │ │ │ │ │ │ ├── fixtures/ │ │ │ │ │ │ │ ├── 30530d66-37fb-4f3b-aa5f-d92b6a8afae2.jsonl │ │ │ │ │ │ │ ├── 50a7220d-7250-46f3-b38e-b716ce25032e/ │ │ │ │ │ │ │ │ └── subagents/ │ │ │ │ │ │ │ │ └── agent-a21e2f5.jsonl │ │ │ │ │ │ │ ├── 50a7220d-7250-46f3-b38e-b716ce25032e.jsonl │ │ │ │ │ │ │ ├── 553dd2b5-8a53-4fbf-9db2-240632522fe5.jsonl │ │ │ │ │ │ │ ├── b02ed4d8-1f00-45cc-949f-3ea63b2dbde2.jsonl │ │ │ │ │ │ │ ├── b3a7bd3c-5a10-4e7b-8ff0-7fc0cd6d1093/ │ │ │ │ │ │ │ │ └── subagents/ │ │ │ │ │ │ │ │ ├── agent-a775a67.jsonl │ │ │ │ │ │ │ │ ├── agent-aa9d784.jsonl │ │ │ │ │ │ │ │ ├── agent-ac47f8c.jsonl │ │ │ │ │ │ │ │ └── agent-ae52dab.jsonl │ │ │ │ │ │ │ ├── b3a7bd3c-5a10-4e7b-8ff0-7fc0cd6d1093.jsonl │ │ │ │ │ │ │ └── c8bcb3a7-8728-4d76-9aae-1cbaf2350114.jsonl │ │ │ │ │ │ ├── mockClaudeCodeModels.ts │ │ │ │ │ │ ├── mockClaudeCodeSdkService.ts │ │ │ │ │ │ ├── mockClaudeToolPermissionService.ts │ │ │ │ │ │ ├── planModeHook.spec.ts │ │ │ │ │ │ └── resolvePromptToContentBlocks.spec.ts │ │ │ │ │ └── toolPermissionHandlers/ │ │ │ │ │ ├── editToolHandler.ts │ │ │ │ │ └── index.ts │ │ │ │ └── vscode-node/ │ │ │ │ ├── claudeSlashCommandService.ts │ │ │ │ ├── hooks/ │ │ │ │ │ └── index.ts │ │ │ │ ├── mcpServers/ │ │ │ │ │ └── index.ts │ │ │ │ ├── slashCommands/ │ │ │ │ │ ├── agentsCommand.ts │ │ │ │ │ ├── claudeSlashCommandRegistry.ts │ │ │ │ │ ├── hooksCommand.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── memoryCommand.ts │ │ │ │ │ ├── terminalCommand.ts │ │ │ │ │ └── test/ │ │ │ │ │ ├── claudeSlashCommandRegistry.spec.ts │ │ │ │ │ └── terminalCommand.spec.ts │ │ │ │ ├── test/ │ │ │ │ │ └── claudeSlashCommandService.spec.ts │ │ │ │ └── toolPermissionHandlers/ │ │ │ │ └── index.ts │ │ │ ├── common/ │ │ │ │ ├── agentSessionsWorkspace.ts │ │ │ │ ├── chatCustomAgentsService.ts │ │ │ │ ├── chatSessionMetadataStore.ts │ │ │ │ ├── chatSessionWorkspaceFolderService.ts │ │ │ │ ├── chatSessionWorktreeCheckpointService.ts │ │ │ │ ├── chatSessionWorktreeService.ts │ │ │ │ ├── externalEditTracker.ts │ │ │ │ ├── folderRepositoryManager.ts │ │ │ │ ├── test/ │ │ │ │ │ ├── externalEditTracker.spec.ts │ │ │ │ │ ├── mockChatSessionMetadataStore.ts │ │ │ │ │ └── ttlCache.spec.ts │ │ │ │ ├── ttlCache.ts │ │ │ │ ├── utils.ts │ │ │ │ └── workspaceInfo.ts │ │ │ ├── copilotcli/ │ │ │ │ ├── common/ │ │ │ │ │ ├── copilotCLIPrompt.ts │ │ │ │ │ ├── copilotCLITools.ts │ │ │ │ │ ├── customSessionTitleService.ts │ │ │ │ │ ├── delegationSummaryService.ts │ │ │ │ │ └── test/ │ │ │ │ │ └── copilotCLITools.spec.ts │ │ │ │ ├── node/ │ │ │ │ │ ├── cliHelpers.ts │ │ │ │ │ ├── copilotCLIImageSupport.ts │ │ │ │ │ ├── copilotCLISkills.ts │ │ │ │ │ ├── copilotCli.ts │ │ │ │ │ ├── copilotCliBridgeSpanProcessor.ts │ │ │ │ │ ├── copilotcliPromptResolver.ts │ │ │ │ │ ├── copilotcliSession.ts │ │ │ │ │ ├── copilotcliSessionService.ts │ │ │ │ │ ├── logger.ts │ │ │ │ │ ├── mcpHandler.ts │ │ │ │ │ ├── nodePtyShim.ts │ │ │ │ │ ├── permissionHelpers.ts │ │ │ │ │ ├── ripgrepShim.ts │ │ │ │ │ ├── test/ │ │ │ │ │ │ ├── copilotCliAgents.spec.ts │ │ │ │ │ │ ├── copilotCliAuth.spec.ts │ │ │ │ │ │ ├── copilotCliBridgeSpanProcessor.spec.ts │ │ │ │ │ │ ├── copilotCliModels.spec.ts │ │ │ │ │ │ ├── copilotCliSessionService.spec.ts │ │ │ │ │ │ ├── copilotcliPromptResolver.spec.ts │ │ │ │ │ │ ├── copilotcliSession.spec.ts │ │ │ │ │ │ ├── permissionHelpers.spec.ts │ │ │ │ │ │ └── testHelpers.ts │ │ │ │ │ └── userInputHelpers.ts │ │ │ │ └── vscode-node/ │ │ │ │ ├── commands/ │ │ │ │ │ ├── addFileReference.ts │ │ │ │ │ ├── addSelection.ts │ │ │ │ │ ├── context.ts │ │ │ │ │ ├── diffCommands.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── pickSession.ts │ │ │ │ │ └── sendContext.ts │ │ │ │ ├── contribution.ts │ │ │ │ ├── copilotCLISessionTracker.ts │ │ │ │ ├── customSessionTitleServiceImpl.ts │ │ │ │ ├── diffState.ts │ │ │ │ ├── inProcHttpServer.ts │ │ │ │ ├── lockFile.ts │ │ │ │ ├── readonlyContentProvider.ts │ │ │ │ ├── test/ │ │ │ │ │ ├── addFileReference.spec.ts │ │ │ │ │ ├── addSelection.spec.ts │ │ │ │ │ ├── closeAllForSession.spec.ts │ │ │ │ │ ├── closeDiff.spec.ts │ │ │ │ │ ├── context.spec.ts │ │ │ │ │ ├── copilotCLISessionTracker.spec.ts │ │ │ │ │ ├── diagnosticsChanged.spec.ts │ │ │ │ │ ├── diffCommands.spec.ts │ │ │ │ │ ├── diffState.spec.ts │ │ │ │ │ ├── getDiagnostics.spec.ts │ │ │ │ │ ├── getSelection.spec.ts │ │ │ │ │ ├── getVscodeInfo.spec.ts │ │ │ │ │ ├── inProcHttpServer.spec.ts │ │ │ │ │ ├── lockFile.spec.ts │ │ │ │ │ ├── openDiff.spec.ts │ │ │ │ │ ├── readonlyContentProvider.spec.ts │ │ │ │ │ ├── selectionChanged.spec.ts │ │ │ │ │ ├── testHelpers.ts │ │ │ │ │ └── updateSessionName.spec.ts │ │ │ │ └── tools/ │ │ │ │ ├── closeDiff.ts │ │ │ │ ├── getDiagnostics.ts │ │ │ │ ├── getSelection.ts │ │ │ │ ├── getVscodeInfo.ts │ │ │ │ ├── index.ts │ │ │ │ ├── openDiff.ts │ │ │ │ ├── push/ │ │ │ │ │ ├── diagnosticsChanged.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── selectionChanged.ts │ │ │ │ ├── updateSessionName.ts │ │ │ │ └── utils.ts │ │ │ ├── vscode/ │ │ │ │ ├── chatSessionsUriHandler.ts │ │ │ │ ├── copilotCodingAgentUtils.ts │ │ │ │ └── test/ │ │ │ │ └── copilotCodingAgentUtils.spec.ts │ │ │ └── vscode-node/ │ │ │ ├── agentSessionsWorkspace.ts │ │ │ ├── askUserQuestionHandler.ts │ │ │ ├── chatCustomAgentsService.ts │ │ │ ├── chatHistoryBuilder.ts │ │ │ ├── chatSessionMetadataStoreImpl.ts │ │ │ ├── chatSessionRepositoryTracker.ts │ │ │ ├── chatSessionWorkspaceFolderServiceImpl.ts │ │ │ ├── chatSessionWorktreeCheckpointServiceImpl.ts │ │ │ ├── chatSessionWorktreeServiceImpl.ts │ │ │ ├── chatSessions.ts │ │ │ ├── claudeChatSessionContentProvider.ts │ │ │ ├── copilotCLIChatSessionsContribution.ts │ │ │ ├── copilotCLIPromptReferences.ts │ │ │ ├── copilotCLIPythonEnvironmentApi.ts │ │ │ ├── copilotCLIPythonTerminalService.ts │ │ │ ├── copilotCLIShim.ps1 │ │ │ ├── copilotCLIShim.ts │ │ │ ├── copilotCLITerminalIntegration.ts │ │ │ ├── copilotCLITerminalLinkProvider.ts │ │ │ ├── copilotCloudGitOperationsManager.ts │ │ │ ├── copilotCloudSessionContentBuilder.ts │ │ │ ├── copilotCloudSessionsProvider.ts │ │ │ ├── folderRepositoryManagerImpl.ts │ │ │ ├── prContentProvider.ts │ │ │ ├── pullRequestFileChangesService.ts │ │ │ └── test/ │ │ │ ├── __snapshots__/ │ │ │ │ └── chatSessionContentProvider.spec.ts.snap │ │ │ ├── askUserQuestionHandler.spec.ts │ │ │ ├── chatHistoryBuilder.spec.ts │ │ │ ├── chatSessionMetadataStoreImpl.spec.ts │ │ │ ├── chatSessionWorkspaceFolderService.spec.ts │ │ │ ├── claudeChatSessionContentProvider.spec.ts │ │ │ ├── copilotCLIChatSessionParticipant.spec.ts │ │ │ ├── copilotCLISDKUpgrade.spec.ts │ │ │ ├── copilotCLITerminalIntegration.spec.ts │ │ │ ├── copilotCLITerminalLinkProvider.spec.ts │ │ │ ├── fixtures/ │ │ │ │ ├── 4c289ca8-f8bb-4588-8400-88b78beb784d.jsonl │ │ │ │ ├── 98b76fb9-f5d3-40c5-ab82-b970c20e3764.jsonl │ │ │ │ └── bd937e2a-89e9-4d7b-8125-293a35863fa4.jsonl │ │ │ └── folderRepositoryManager.spec.ts │ │ ├── codeBlocks/ │ │ │ ├── node/ │ │ │ │ ├── codeBlockProcessor.ts │ │ │ │ └── test/ │ │ │ │ └── codeBlockProcessor.spec.ts │ │ │ └── vscode-node/ │ │ │ ├── chatBlockLanguageFeatures.contribution.ts │ │ │ └── provider.ts │ │ ├── commands/ │ │ │ └── node/ │ │ │ └── commandService.ts │ │ ├── common/ │ │ │ ├── constants.ts │ │ │ ├── contributions.ts │ │ │ └── modelContextProtocol.ts │ │ ├── completions/ │ │ │ ├── common/ │ │ │ │ ├── config.ts │ │ │ │ ├── copilotInlineCompletionItemProviderService.ts │ │ │ │ └── parseBlock.ts │ │ │ └── vscode-node/ │ │ │ ├── completionsCoreContribution.ts │ │ │ ├── completionsUnificationContribution.ts │ │ │ └── copilotInlineCompletionItemProviderService.ts │ │ ├── completions-core/ │ │ │ ├── common/ │ │ │ │ └── ghostTextContext.ts │ │ │ └── vscode-node/ │ │ │ ├── bridge/ │ │ │ │ └── src/ │ │ │ │ └── completionsTelemetryServiceBridge.ts │ │ │ ├── completionsServiceBridges.ts │ │ │ ├── extension/ │ │ │ │ ├── src/ │ │ │ │ │ ├── codeReferencing/ │ │ │ │ │ │ ├── citationManager.ts │ │ │ │ │ │ ├── codeReferenceEngagementTracker.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── matchNotifier.ts │ │ │ │ │ │ ├── outputChannel.ts │ │ │ │ │ │ └── test/ │ │ │ │ │ │ ├── codeReferenceEngagementTracker.test.ts │ │ │ │ │ │ ├── codeReferencing.test.ts │ │ │ │ │ │ └── matchNotifier.test.ts │ │ │ │ │ ├── completionsObservableWorkspace.ts │ │ │ │ │ ├── config.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── contextProviderMatch.ts │ │ │ │ │ ├── copilotCompletionFeedbackTracker.ts │ │ │ │ │ ├── copilotPanel/ │ │ │ │ │ │ ├── common.ts │ │ │ │ │ │ ├── copilotListDocument.ts │ │ │ │ │ │ ├── copilotSuggestionsPanel.ts │ │ │ │ │ │ ├── copilotSuggestionsPanelManager.ts │ │ │ │ │ │ ├── panelConfig.ts │ │ │ │ │ │ └── webView/ │ │ │ │ │ │ ├── suggestionsPanelWebview.ts │ │ │ │ │ │ └── tsconfig.json │ │ │ │ │ ├── extensionStatus.ts │ │ │ │ │ ├── fileSystem.ts │ │ │ │ │ ├── ghostText/ │ │ │ │ │ │ └── ghostTextProvider.ts │ │ │ │ │ ├── icon.ts │ │ │ │ │ ├── lib/ │ │ │ │ │ │ ├── copilotPanel/ │ │ │ │ │ │ │ ├── common.ts │ │ │ │ │ │ │ └── panel.ts │ │ │ │ │ │ └── panelShared/ │ │ │ │ │ │ ├── common.ts │ │ │ │ │ │ └── panelTypes.ts │ │ │ │ │ ├── modelPicker.ts │ │ │ │ │ ├── modelPickerUserSelection.ts │ │ │ │ │ ├── panelShared/ │ │ │ │ │ │ ├── baseListDocument.ts │ │ │ │ │ │ ├── basePanelTypes.ts │ │ │ │ │ │ ├── baseSuggestionsPanel.ts │ │ │ │ │ │ ├── baseSuggestionsPanelManager.ts │ │ │ │ │ │ ├── highlighter.ts │ │ │ │ │ │ ├── languages/ │ │ │ │ │ │ │ ├── cuda-cpp.tmLanguage.ts │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── javaScriptReact.tmLanguage.ts │ │ │ │ │ │ │ ├── markdown-latex-combined.tmLanguage.ts │ │ │ │ │ │ │ ├── md-math.tmLanguage.ts │ │ │ │ │ │ │ ├── rst.tmLanguage.ts │ │ │ │ │ │ │ ├── searchResult.tmLanguage.ts │ │ │ │ │ │ │ └── typeScriptReact.tmLanguage.ts │ │ │ │ │ │ ├── themes/ │ │ │ │ │ │ │ ├── abyss.ts │ │ │ │ │ │ │ ├── dark-hc.ts │ │ │ │ │ │ │ ├── dark-modern.ts │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── kimbie-dark.ts │ │ │ │ │ │ │ ├── light-hc.ts │ │ │ │ │ │ │ ├── light-modern.ts │ │ │ │ │ │ │ ├── monokai-dim.ts │ │ │ │ │ │ │ ├── quiet-light.ts │ │ │ │ │ │ │ ├── red.ts │ │ │ │ │ │ │ ├── tomorrow-night-blue.ts │ │ │ │ │ │ │ ├── vs-dark.ts │ │ │ │ │ │ │ └── vs-light.ts │ │ │ │ │ │ └── utils.ts │ │ │ │ │ ├── statusBar.ts │ │ │ │ │ ├── statusBarPicker.ts │ │ │ │ │ ├── telemetry.ts │ │ │ │ │ ├── test/ │ │ │ │ │ │ ├── config.ts │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ └── modelPicker.test.ts │ │ │ │ │ ├── textDocumentManager.ts │ │ │ │ │ └── vscodeInlineCompletionItemProvider.ts │ │ │ │ └── test/ │ │ │ │ ├── run.js │ │ │ │ └── runTest.ts │ │ │ ├── lib/ │ │ │ │ └── src/ │ │ │ │ ├── auth/ │ │ │ │ │ ├── copilotTokenManager.ts │ │ │ │ │ ├── copilotTokenNotifier.ts │ │ │ │ │ └── orgs.ts │ │ │ │ ├── changeTracker.ts │ │ │ │ ├── citationManager.ts │ │ │ │ ├── completionNotifier.ts │ │ │ │ ├── completionState.ts │ │ │ │ ├── completionsObservableWorkspace.ts │ │ │ │ ├── config.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── defaultHandlers.ts │ │ │ │ ├── diagnostics.ts │ │ │ │ ├── documentTracker.ts │ │ │ │ ├── error/ │ │ │ │ │ └── userErrorNotifier.ts │ │ │ │ ├── experiments/ │ │ │ │ │ ├── defaultExpFilters.ts │ │ │ │ │ ├── expConfig.ts │ │ │ │ │ ├── features.ts │ │ │ │ │ ├── featuresService.ts │ │ │ │ │ ├── filters.ts │ │ │ │ │ ├── similarFileOptionsProvider.ts │ │ │ │ │ ├── similarFileOptionsProviderCpp.ts │ │ │ │ │ ├── telemetryNames.ts │ │ │ │ │ └── test/ │ │ │ │ │ └── features.test.ts │ │ │ │ ├── fileReader.ts │ │ │ │ ├── fileSystem.ts │ │ │ │ ├── ghostText/ │ │ │ │ │ ├── asyncCompletions.ts │ │ │ │ │ ├── blockTrimmer.ts │ │ │ │ │ ├── cacheUtils.ts │ │ │ │ │ ├── completionsCache.ts │ │ │ │ │ ├── completionsFromNetwork.ts │ │ │ │ │ ├── configBlockMode.ts │ │ │ │ │ ├── contextualFilterConstants.ts │ │ │ │ │ ├── copilotCompletion.ts │ │ │ │ │ ├── current.ts │ │ │ │ │ ├── ghostText.ts │ │ │ │ │ ├── ghostTextStrategy.ts │ │ │ │ │ ├── last.ts │ │ │ │ │ ├── multilineModel.ts │ │ │ │ │ ├── multilineModelWeights.ts │ │ │ │ │ ├── normalizeIndent.ts │ │ │ │ │ ├── requestContext.ts │ │ │ │ │ ├── resultType.ts │ │ │ │ │ ├── speculativeRequestCache.ts │ │ │ │ │ ├── statementTree.ts │ │ │ │ │ ├── streamedCompletionSplitter.ts │ │ │ │ │ ├── telemetry.ts │ │ │ │ │ └── test/ │ │ │ │ │ ├── asyncCompletions.test.ts │ │ │ │ │ ├── blockTrimmer.test.ts │ │ │ │ │ ├── current.test.ts │ │ │ │ │ ├── ghostText.test.ts │ │ │ │ │ ├── last.test.ts │ │ │ │ │ ├── multilineModel.test.ts │ │ │ │ │ ├── normalizeIndent.test.ts │ │ │ │ │ ├── statementTree.test.ts │ │ │ │ │ └── streamedCompletionSplitter.test.ts │ │ │ │ ├── helpers/ │ │ │ │ │ ├── cache.ts │ │ │ │ │ ├── iterableHelpers.ts │ │ │ │ │ ├── radix.ts │ │ │ │ │ └── test/ │ │ │ │ │ ├── cache.test.ts │ │ │ │ │ ├── iterableHelpers.test.ts │ │ │ │ │ └── radix.test.ts │ │ │ │ ├── inlineCompletion.ts │ │ │ │ ├── language/ │ │ │ │ │ ├── generatedLanguages.ts │ │ │ │ │ ├── languageDetection.ts │ │ │ │ │ ├── languages.ts │ │ │ │ │ └── test/ │ │ │ │ │ ├── generatedLanguages.test.ts │ │ │ │ │ └── languageDetection.test.ts │ │ │ │ ├── localFileSystem.ts │ │ │ │ ├── logger.ts │ │ │ │ ├── logging/ │ │ │ │ │ └── util.ts │ │ │ │ ├── networkConfiguration.ts │ │ │ │ ├── networking.ts │ │ │ │ ├── networkingTypes.ts │ │ │ │ ├── notificationSender.ts │ │ │ │ ├── openai/ │ │ │ │ │ ├── config.ts │ │ │ │ │ ├── fetch.fake.ts │ │ │ │ │ ├── fetch.ts │ │ │ │ │ ├── model.ts │ │ │ │ │ ├── openai.ts │ │ │ │ │ ├── stream.ts │ │ │ │ │ └── test/ │ │ │ │ │ ├── config.test.ts │ │ │ │ │ ├── fetch.test.ts │ │ │ │ │ └── stream.test.ts │ │ │ │ ├── postInsertion.ts │ │ │ │ ├── progress.ts │ │ │ │ ├── prompt/ │ │ │ │ │ ├── asyncUtils.ts │ │ │ │ │ ├── completionsPromptFactory/ │ │ │ │ │ │ ├── cascadingPromptFactory.ts │ │ │ │ │ │ ├── completionsPromptFactory.ts │ │ │ │ │ │ ├── componentsCompletionsPromptFactory.tsx │ │ │ │ │ │ └── test/ │ │ │ │ │ │ └── completionsPromptFactory.test.tsx │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── codeSnippets.tsx │ │ │ │ │ │ ├── completionsContext.tsx │ │ │ │ │ │ ├── completionsPromptRenderer.tsx │ │ │ │ │ │ ├── contextProviderBridge.ts │ │ │ │ │ │ ├── currentFile.tsx │ │ │ │ │ │ ├── diagnostics.tsx │ │ │ │ │ │ ├── elision.ts │ │ │ │ │ │ ├── marker.tsx │ │ │ │ │ │ ├── recentEdits.tsx │ │ │ │ │ │ ├── similarFiles.tsx │ │ │ │ │ │ ├── splitContextPrompt.tsx │ │ │ │ │ │ ├── splitContextPromptRenderer.tsx │ │ │ │ │ │ ├── test/ │ │ │ │ │ │ │ ├── codeSnippets.test.tsx │ │ │ │ │ │ │ ├── completionsPromptRenderer.test.tsx │ │ │ │ │ │ │ ├── contextProviderBridge.test.ts │ │ │ │ │ │ │ ├── currentFile.test.tsx │ │ │ │ │ │ │ ├── marker.test.tsx │ │ │ │ │ │ │ ├── recentEdits.test.tsx │ │ │ │ │ │ │ ├── similarFiles.test.tsx │ │ │ │ │ │ │ ├── splitContextPromptRenderer.test.tsx │ │ │ │ │ │ │ └── traits.test.tsx │ │ │ │ │ │ ├── traits.tsx │ │ │ │ │ │ └── virtualComponent.ts │ │ │ │ │ ├── contextProviderRegistry.ts │ │ │ │ │ ├── contextProviderRegistryCSharp.ts │ │ │ │ │ ├── contextProviderRegistryCpp.ts │ │ │ │ │ ├── contextProviderRegistryMultiLanguage.ts │ │ │ │ │ ├── contextProviderRegistryTs.ts │ │ │ │ │ ├── contextProviderStatistics.ts │ │ │ │ │ ├── contextProviders/ │ │ │ │ │ │ ├── codeSnippets.ts │ │ │ │ │ │ ├── contextItemSchemas.ts │ │ │ │ │ │ ├── diagnostics.ts │ │ │ │ │ │ ├── test/ │ │ │ │ │ │ │ ├── codeSnippets.test.ts │ │ │ │ │ │ │ ├── contextItemSchemas.test.ts │ │ │ │ │ │ │ └── traits.test.ts │ │ │ │ │ │ └── traits.ts │ │ │ │ │ ├── parseBlock.ts │ │ │ │ │ ├── prompt.ts │ │ │ │ │ ├── recentEdits/ │ │ │ │ │ │ ├── emptyRecentEditsProvider.ts │ │ │ │ │ │ ├── recentEditsProvider.ts │ │ │ │ │ │ ├── recentEditsReducer.ts │ │ │ │ │ │ └── test/ │ │ │ │ │ │ └── recentEditsReducer.test.ts │ │ │ │ │ ├── render/ │ │ │ │ │ │ ├── renderNode.ts │ │ │ │ │ │ ├── test/ │ │ │ │ │ │ │ └── renderNode.test.ts │ │ │ │ │ │ └── utils.ts │ │ │ │ │ ├── repository.ts │ │ │ │ │ ├── similarFiles/ │ │ │ │ │ │ ├── compositeRelatedFilesProvider.ts │ │ │ │ │ │ ├── neighborFiles.ts │ │ │ │ │ │ ├── openTabFiles.ts │ │ │ │ │ │ ├── relatedFiles.ts │ │ │ │ │ │ └── test/ │ │ │ │ │ │ ├── neighborFiles.test.ts │ │ │ │ │ │ └── relatedFiles.test.ts │ │ │ │ │ └── test/ │ │ │ │ │ ├── contextProviderRegistry.test.ts │ │ │ │ │ ├── contextProviderRegistryMultiLanguage.test.ts │ │ │ │ │ ├── contextProviderRegistryTs.test.ts │ │ │ │ │ ├── contextProviderStatistics.test.ts │ │ │ │ │ ├── contextProviderStatistics.ts │ │ │ │ │ ├── contextProviderTelemetry.ts │ │ │ │ │ ├── defaultDiagnosticSettings.test.ts │ │ │ │ │ ├── determineTimeComplexity.ts │ │ │ │ │ ├── parseBlock.test.ts │ │ │ │ │ ├── prompt.test.ts │ │ │ │ │ ├── prompt.ts │ │ │ │ │ ├── relatedFiles.ts │ │ │ │ │ └── repository.test.ts │ │ │ │ ├── snippy/ │ │ │ │ │ ├── compute.ts │ │ │ │ │ ├── connectionState.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── errorCreator.ts │ │ │ │ │ ├── handlePostInsertion.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logger.ts │ │ │ │ │ ├── network.ts │ │ │ │ │ ├── snippy.proto.ts │ │ │ │ │ ├── telemetryHandlers.ts │ │ │ │ │ └── test/ │ │ │ │ │ ├── compute.test.ts │ │ │ │ │ └── network.test.ts │ │ │ │ ├── suggestions/ │ │ │ │ │ ├── anomalyDetection.ts │ │ │ │ │ ├── editDistance.ts │ │ │ │ │ ├── partialSuggestions.ts │ │ │ │ │ ├── suggestions.ts │ │ │ │ │ └── test/ │ │ │ │ │ ├── anomalyDetection.test.ts │ │ │ │ │ ├── editDistance.test.ts │ │ │ │ │ ├── partialSuggestions.test.ts │ │ │ │ │ └── suggestions.test.ts │ │ │ │ ├── telemetry/ │ │ │ │ │ └── userConfig.ts │ │ │ │ ├── telemetry.ts │ │ │ │ ├── test/ │ │ │ │ │ ├── changeTracker.test.ts │ │ │ │ │ ├── completionNotifier.test.ts │ │ │ │ │ ├── completionState.test.ts │ │ │ │ │ ├── completionsPrompt.ts │ │ │ │ │ ├── config.test.ts │ │ │ │ │ ├── context.ts │ │ │ │ │ ├── copilotTokenManager.ts │ │ │ │ │ ├── fetcher.ts │ │ │ │ │ ├── fileReader.test.ts │ │ │ │ │ ├── filesystem.ts │ │ │ │ │ ├── inlineCompletion.test.ts │ │ │ │ │ ├── localFileSystem.test.ts │ │ │ │ │ ├── loggerHelpers.ts │ │ │ │ │ ├── networking.test.ts │ │ │ │ │ ├── noopTelemetry.ts │ │ │ │ │ ├── notificationSender.test.ts │ │ │ │ │ ├── postInsertion.test.ts │ │ │ │ │ ├── runtimeMode.test.ts │ │ │ │ │ ├── snapshot.ts │ │ │ │ │ ├── telemetry.test.ts │ │ │ │ │ ├── telemetry.ts │ │ │ │ │ ├── telemetrySpy.ts │ │ │ │ │ ├── testContentExclusion.ts │ │ │ │ │ ├── testHelpers.ts │ │ │ │ │ ├── textDocument.test.ts │ │ │ │ │ ├── textDocument.ts │ │ │ │ │ └── textDocumentManager.test.ts │ │ │ │ ├── textDocument.ts │ │ │ │ ├── textDocumentManager.ts │ │ │ │ └── util/ │ │ │ │ ├── async.ts │ │ │ │ ├── documentEvaluation.ts │ │ │ │ ├── event.ts │ │ │ │ ├── map.ts │ │ │ │ ├── priorityQueue.ts │ │ │ │ ├── promiseQueue.ts │ │ │ │ ├── runtimeMode.ts │ │ │ │ ├── shortCircuit.ts │ │ │ │ ├── subject.ts │ │ │ │ ├── test/ │ │ │ │ │ ├── async.test.ts │ │ │ │ │ ├── priorityQueue.test.ts │ │ │ │ │ ├── shortCircuit.test.ts │ │ │ │ │ ├── subject.test.ts │ │ │ │ │ └── uri.test.ts │ │ │ │ ├── typebox.ts │ │ │ │ ├── unknown.ts │ │ │ │ └── uri.ts │ │ │ ├── prompt/ │ │ │ │ ├── jsx-runtime/ │ │ │ │ │ └── jsx-runtime.ts │ │ │ │ └── src/ │ │ │ │ ├── components/ │ │ │ │ │ ├── components.ts │ │ │ │ │ ├── hooks.ts │ │ │ │ │ ├── reconciler.ts │ │ │ │ │ ├── virtualPrompt.ts │ │ │ │ │ └── walker.ts │ │ │ │ ├── error.ts │ │ │ │ ├── fileLoader.ts │ │ │ │ ├── indentation/ │ │ │ │ │ ├── classes.ts │ │ │ │ │ ├── description.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── java.ts │ │ │ │ │ ├── manipulation.ts │ │ │ │ │ ├── markdown.ts │ │ │ │ │ └── parsing.ts │ │ │ │ ├── languageMarker.ts │ │ │ │ ├── parse.ts │ │ │ │ ├── parseBlock.ts │ │ │ │ ├── prompt.ts │ │ │ │ ├── snippetInclusion/ │ │ │ │ │ ├── cursorContext.ts │ │ │ │ │ ├── jaccardMatching.ts │ │ │ │ │ ├── selectRelevance.ts │ │ │ │ │ ├── similarFiles.ts │ │ │ │ │ ├── snippets.ts │ │ │ │ │ ├── subsetMatching.ts │ │ │ │ │ └── windowDelineations.ts │ │ │ │ ├── suffixMatchCriteria.ts │ │ │ │ ├── test/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── hooks.test.ts │ │ │ │ │ │ ├── jsx-runtime.test.ts.off │ │ │ │ │ │ ├── reconciler.test.tsx │ │ │ │ │ │ ├── testHelpers.ts │ │ │ │ │ │ ├── virtualPrompt.test.tsx │ │ │ │ │ │ └── walker.test.ts │ │ │ │ │ ├── indentation.test.ts │ │ │ │ │ ├── indentationLanguages.test.ts │ │ │ │ │ ├── indentationParsing.test.ts │ │ │ │ │ ├── languageMarker.test.ts │ │ │ │ │ ├── multisnippet.test.ts │ │ │ │ │ ├── parse.test.ts │ │ │ │ │ ├── parseBlock.test.ts │ │ │ │ │ ├── similarFiles.test.ts │ │ │ │ │ ├── snippets.test.ts │ │ │ │ │ ├── subsetMatching.test.ts │ │ │ │ │ ├── suffixmatch.test.ts │ │ │ │ │ ├── testHelpers.ts │ │ │ │ │ ├── testdata/ │ │ │ │ │ │ ├── example.py │ │ │ │ │ │ ├── lazy_greet.py │ │ │ │ │ │ ├── testTokenizer.ts │ │ │ │ │ │ └── testWishlist.ts │ │ │ │ │ ├── tokenizer.test.ts │ │ │ │ │ └── windowDelineation.test.ts │ │ │ │ └── tokenization/ │ │ │ │ ├── index.ts │ │ │ │ └── tokenizer.ts │ │ │ └── types/ │ │ │ └── src/ │ │ │ ├── auth.ts │ │ │ ├── codeCitation.ts │ │ │ ├── contextProviderApiV1.ts │ │ │ ├── core.ts │ │ │ ├── index.ts │ │ │ └── status.ts │ │ ├── configuration/ │ │ │ └── vscode-node/ │ │ │ └── configurationMigration.ts │ │ ├── context/ │ │ │ ├── node/ │ │ │ │ └── resolvers/ │ │ │ │ ├── extensionApi.tsx │ │ │ │ ├── fixSelection.ts │ │ │ │ ├── genericInlineIntentInvocation.ts │ │ │ │ ├── genericPanelIntentInvocation.ts │ │ │ │ ├── inlineChatSelection.ts │ │ │ │ ├── inlineFixIntentInvocation.ts │ │ │ │ ├── promptWorkspaceLabels.ts │ │ │ │ ├── selectionContextHelpers.ts │ │ │ │ ├── test/ │ │ │ │ │ └── vscodeContext.spec.ts │ │ │ │ └── vscodeContext.ts │ │ │ └── vscode/ │ │ │ └── context.contribution.ts │ │ ├── contextKeys/ │ │ │ └── vscode-node/ │ │ │ └── contextKeys.contribution.ts │ │ ├── conversation/ │ │ │ ├── common/ │ │ │ │ └── languageModelChatMessageHelpers.ts │ │ │ ├── node/ │ │ │ │ ├── aiMappedEditsProvider.ts │ │ │ │ └── githubPullRequestProviders.ts │ │ │ └── vscode-node/ │ │ │ ├── aiMappedEditsContrib.ts │ │ │ ├── chatParticipants.ts │ │ │ ├── conversationFeature.ts │ │ │ ├── feedbackCollection.ts │ │ │ ├── feedbackContribution.ts │ │ │ ├── feedbackReporter.ts │ │ │ ├── languageModelAccess.ts │ │ │ ├── languageModelAccessPrompt.tsx │ │ │ ├── logWorkspaceState.ts │ │ │ ├── newWorkspaceFollowup.ts │ │ │ ├── remoteAgents.ts │ │ │ ├── resolveModelId.ts │ │ │ ├── terminalFixGenerator.ts │ │ │ ├── test/ │ │ │ │ ├── conversationFeature.test.ts │ │ │ │ ├── githubPullRequestTitleAndDescription.test.ts │ │ │ │ ├── interactiveEditorSessionProvider.test.ts │ │ │ │ ├── interactiveSessionProvider.telemetry.test.ts │ │ │ │ ├── languageModelAccess.test.ts │ │ │ │ └── userActionsResolvedModel.spec.ts │ │ │ ├── userActions.ts │ │ │ └── welcomeMessageProvider.ts │ │ ├── conversationStore/ │ │ │ └── node/ │ │ │ └── conversationStore.ts │ │ ├── diagnosticsContext/ │ │ │ └── vscode/ │ │ │ └── diagnosticsContextProvider.ts │ │ ├── extension/ │ │ │ ├── vscode/ │ │ │ │ ├── contributions.ts │ │ │ │ ├── extension.ts │ │ │ │ └── services.ts │ │ │ ├── vscode-node/ │ │ │ │ ├── contributions.ts │ │ │ │ ├── extension.ts │ │ │ │ └── services.ts │ │ │ └── vscode-worker/ │ │ │ ├── contributions.ts │ │ │ ├── extension.ts │ │ │ └── services.ts │ │ ├── externalAgents/ │ │ │ ├── node/ │ │ │ │ ├── modelProxyProvider.ts │ │ │ │ └── oaiLanguageModelServer.ts │ │ │ └── vscode-node/ │ │ │ └── lmProxyContrib.ts │ │ ├── getting-started/ │ │ │ ├── common/ │ │ │ │ └── newWorkspaceContext.ts │ │ │ └── vscode-node/ │ │ │ ├── commands.ts │ │ │ ├── newWorkspace.contribution.ts │ │ │ └── newWorkspaceInitializer.ts │ │ ├── git/ │ │ │ ├── common/ │ │ │ │ └── mergeConflictService.ts │ │ │ └── vscode/ │ │ │ ├── mergeConflictParser.ts │ │ │ ├── mergeConflictServiceImpl.ts │ │ │ └── scmContextprovider.ts │ │ ├── githubMcp/ │ │ │ ├── common/ │ │ │ │ └── githubMcpDefinitionProvider.ts │ │ │ ├── test/ │ │ │ │ └── node/ │ │ │ │ └── githubMcpDefinitionProvider.spec.ts │ │ │ └── vscode-node/ │ │ │ └── githubMcp.contribution.ts │ │ ├── githubPullRequest.d.ts │ │ ├── ignore/ │ │ │ └── vscode-node/ │ │ │ ├── ignoreMessage.ts │ │ │ └── ignoreProvider.ts │ │ ├── inlineChat/ │ │ │ ├── node/ │ │ │ │ ├── codeContextRegion.ts │ │ │ │ ├── diagnosticsTelemetry.ts │ │ │ │ ├── inlineChatConstants.ts │ │ │ │ ├── inlineChatIntent.ts │ │ │ │ ├── progressMessages.ts │ │ │ │ ├── promptCraftingTypes.ts │ │ │ │ └── rendererVisualization.ts │ │ │ ├── test/ │ │ │ │ └── vscode-node/ │ │ │ │ ├── inlineChat.test.ts │ │ │ │ └── naturalLanguageHint.test.ts │ │ │ └── vscode-node/ │ │ │ ├── inlineChatCodeActions.ts │ │ │ ├── inlineChatCommands.ts │ │ │ ├── inlineChatNotebookActions.ts │ │ │ └── naturalLanguageHint.ts │ │ ├── inlineEdits/ │ │ │ ├── common/ │ │ │ │ ├── common.ts │ │ │ │ ├── correlationId.ts │ │ │ │ ├── delay.ts │ │ │ │ ├── editRebase.ts │ │ │ │ ├── informationDelta.tsx │ │ │ │ ├── nearbyCursorInlineEditProvider.ts │ │ │ │ ├── nesTriggerHint.ts │ │ │ │ ├── observableWorkspaceRecordingReplayer.ts │ │ │ │ ├── rejectionCollector.ts │ │ │ │ └── userInteractionMonitor.ts │ │ │ ├── node/ │ │ │ │ ├── createNextEditProvider.ts │ │ │ │ ├── debugRecorder.ts │ │ │ │ ├── diffNextEdits.ts │ │ │ │ ├── importFiltering.ts │ │ │ │ ├── nesConfigs.ts │ │ │ │ ├── nextEditCache.ts │ │ │ │ ├── nextEditProvider.ts │ │ │ │ ├── nextEditProviderTelemetry.ts │ │ │ │ ├── nextEditResult.ts │ │ │ │ └── rebaseResult.ts │ │ │ ├── test/ │ │ │ │ ├── common/ │ │ │ │ │ ├── editRebase.spec.ts │ │ │ │ │ ├── userHappinessScore.spec.ts │ │ │ │ │ └── userInteractionMonitor.spec.ts │ │ │ │ ├── node/ │ │ │ │ │ ├── debugRecorder.spec.ts │ │ │ │ │ ├── fileLoading.ts │ │ │ │ │ ├── ignoreImportChanges.spec.ts │ │ │ │ │ ├── nesXtabHistoryTracker.spec.ts │ │ │ │ │ ├── nextEditProviderCaching.spec.ts │ │ │ │ │ ├── nextEditProviderSpeculative.spec.ts │ │ │ │ │ ├── nextEditProviderTelemetry.spec.ts │ │ │ │ │ ├── recordings/ │ │ │ │ │ │ ├── ArrayToObject.recording.w.json │ │ │ │ │ │ ├── ChangePointToPoint3D.recording.w.json │ │ │ │ │ │ ├── DeclaringConstructorArgument.recording.w.json │ │ │ │ │ │ ├── EditSourceTracker.test1.recording.w.json │ │ │ │ │ │ └── RejectionCollector.test1.w.json │ │ │ │ │ ├── rejectionCollector.spec.ts │ │ │ │ │ └── runRecording.ts │ │ │ │ └── vscode-node/ │ │ │ │ ├── diagnosticsCollection.spec.ts │ │ │ │ ├── documentFilter.ts │ │ │ │ ├── inlineEditTriggerer.spec.ts │ │ │ │ ├── isInlineSuggestion.spec.ts │ │ │ │ ├── isSubword.spec.ts │ │ │ │ └── raceAndAll.spec.ts │ │ │ └── vscode-node/ │ │ │ ├── components/ │ │ │ │ ├── expectedEditCaptureController.ts │ │ │ │ ├── inlineEditDebugComponent.ts │ │ │ │ ├── logContextRecorder.ts │ │ │ │ ├── nesFeedbackSubmitter.ts │ │ │ │ └── test/ │ │ │ │ ├── inlineEditDebugComponent.spec.ts │ │ │ │ └── nesFeedbackSubmitter.spec.ts │ │ │ ├── features/ │ │ │ │ ├── diagnosticsBasedCompletions/ │ │ │ │ │ ├── anyDiagnosticsCompletionProvider.ts │ │ │ │ │ ├── asyncDiagnosticsCompletionProvider.ts │ │ │ │ │ ├── diagnosticsCompletions.ts │ │ │ │ │ └── importDiagnosticsCompletionProvider.ts │ │ │ │ ├── diagnosticsCompletionProcessor.ts │ │ │ │ └── diagnosticsInlineEditProvider.ts │ │ │ ├── inlineCompletionProvider.ts │ │ │ ├── inlineEditModel.ts │ │ │ ├── inlineEditProviderFeature.ts │ │ │ ├── inlineEditTriggerer.ts │ │ │ ├── isInlineSuggestion.ts │ │ │ ├── jointInlineCompletionProvider.ts │ │ │ ├── parts/ │ │ │ │ ├── common.ts │ │ │ │ ├── documentFilter.ts │ │ │ │ ├── inlineEditLogger.ts │ │ │ │ ├── verifyTextDocumentChanges.ts │ │ │ │ └── vscodeWorkspace.ts │ │ │ ├── raceAndAll.ts │ │ │ ├── similarFilesContext.ts │ │ │ └── utils/ │ │ │ ├── observablesUtils.ts │ │ │ ├── translations.ts │ │ │ └── virtualTextDocumentProvider.ts │ │ ├── intents/ │ │ │ ├── common/ │ │ │ │ ├── agentConfig.ts │ │ │ │ └── intents.ts │ │ │ ├── node/ │ │ │ │ ├── agentIntent.ts │ │ │ │ ├── allIntents.ts │ │ │ │ ├── askAgentIntent.ts │ │ │ │ ├── cacheBreakpoints.ts │ │ │ │ ├── docIntent.tsx │ │ │ │ ├── editCodeIntent.ts │ │ │ │ ├── editCodeIntent2.ts │ │ │ │ ├── editCodeStep.ts │ │ │ │ ├── explainIntent.ts │ │ │ │ ├── fixIntent.ts │ │ │ │ ├── generateCodeIntent.ts │ │ │ │ ├── generateNewWorkspaceContent.ts │ │ │ │ ├── hookResultProcessor.ts │ │ │ │ ├── intentService.ts │ │ │ │ ├── newIntent.ts │ │ │ │ ├── newNotebookIntent.contribution.ts │ │ │ │ ├── newNotebookIntent.ts │ │ │ │ ├── notebookEditorIntent.ts │ │ │ │ ├── promptOverride.ts │ │ │ │ ├── reviewIntent.ts │ │ │ │ ├── searchIntent.ts │ │ │ │ ├── searchKeywordsIntent.ts │ │ │ │ ├── searchPanelIntent.ts │ │ │ │ ├── setupTests.ts │ │ │ │ ├── terminalExplainIntent.ts │ │ │ │ ├── terminalIntent.ts │ │ │ │ ├── test/ │ │ │ │ │ ├── agentSummarizeCommand.spec.ts │ │ │ │ │ └── promptOverride.spec.ts │ │ │ │ ├── testIntent/ │ │ │ │ │ ├── setupTestsFrameworkQueryInvocation.tsx │ │ │ │ │ ├── setupTestsInvocation.tsx │ │ │ │ │ ├── summarizedDocumentWithSelection.tsx │ │ │ │ │ ├── testDeps.tsx │ │ │ │ │ ├── testFromSrcInvocation.tsx │ │ │ │ │ ├── testFromTestInvocation.tsx │ │ │ │ │ ├── testInfoStorage.ts │ │ │ │ │ ├── testIntent.tsx │ │ │ │ │ ├── testPromptUtil.ts │ │ │ │ │ └── userQueryParser.tsx │ │ │ │ ├── toolCallingLoop.ts │ │ │ │ ├── unknownIntent.ts │ │ │ │ └── vscodeIntent.ts │ │ │ ├── test/ │ │ │ │ └── node/ │ │ │ │ ├── editCodeIntent.spec.ts │ │ │ │ ├── hookResultProcessor.spec.ts │ │ │ │ ├── toolCallingLoopAutopilot.spec.ts │ │ │ │ ├── toolCallingLoopHooks.spec.ts │ │ │ │ ├── toolCallingLoopUsage.spec.ts │ │ │ │ └── validateToolMessages.spec.ts │ │ │ └── vscode-node/ │ │ │ ├── fixTestFailureContributions.ts │ │ │ ├── newWorkspacePreviewFileSystemProvider.ts │ │ │ └── newWorkspaceTextDocumentProvider.ts │ │ ├── languageContextProvider/ │ │ │ └── vscode-node/ │ │ │ └── languageContextProviderService.ts │ │ ├── linkify/ │ │ │ ├── common/ │ │ │ │ ├── commands.ts │ │ │ │ ├── filePathLinkifier.ts │ │ │ │ ├── linkifiedText.ts │ │ │ │ ├── linkifier.ts │ │ │ │ ├── linkifyService.ts │ │ │ │ ├── modelFilePathLinkifier.ts │ │ │ │ ├── responseStreamWithLinkification.ts │ │ │ │ └── statCache.ts │ │ │ ├── test/ │ │ │ │ ├── node/ │ │ │ │ │ ├── filePathLinkifier.spec.ts │ │ │ │ │ ├── linkifier.spec.ts │ │ │ │ │ ├── modelFilePathLinkifier.spec.ts │ │ │ │ │ ├── statCaching.spec.ts │ │ │ │ │ └── util.ts │ │ │ │ └── vscode-node/ │ │ │ │ ├── findSymbol.test.ts │ │ │ │ ├── notebookCellLinkifier.spec.ts │ │ │ │ └── symbolLinkifier.test.ts │ │ │ └── vscode-node/ │ │ │ ├── commands.ts │ │ │ ├── findSymbol.ts │ │ │ ├── findWord.ts │ │ │ ├── inlineCodeSymbolLinkifier.ts │ │ │ ├── notebookCellLinkifier.ts │ │ │ └── symbolLinkifier.ts │ │ ├── log/ │ │ │ └── vscode-node/ │ │ │ ├── extensionStateCommand.ts │ │ │ ├── loggingActions.ts │ │ │ ├── requestLogTree.ts │ │ │ └── test/ │ │ │ └── sanitizer.spec.ts │ │ ├── mcp/ │ │ │ ├── test/ │ │ │ │ └── vscode-node/ │ │ │ │ ├── commands.spec.ts │ │ │ │ ├── fixtures/ │ │ │ │ │ ├── nuget/ │ │ │ │ │ │ ├── basetestpackage.dotnettool.1.0.0.nupkg │ │ │ │ │ │ ├── basetestpackage.mcpserver.0.1.0-beta.nupkg │ │ │ │ │ │ └── knapcode.samplemcpserver.0.6.0-beta.nupkg │ │ │ │ │ └── snapshots/ │ │ │ │ │ ├── docker-mcp-node-code-sandbox.json │ │ │ │ │ ├── dotnet-package-search-does-not-exist.json │ │ │ │ │ ├── dotnet-package-search-exists.json │ │ │ │ │ ├── npm-modelcontextprotocol-server-everything.json │ │ │ │ │ ├── nuget-readme.md │ │ │ │ │ ├── nuget-service-index.json │ │ │ │ │ └── pip-mcp-server-fetch.json │ │ │ │ ├── nuget.integration.spec.ts │ │ │ │ ├── nuget.mapping.spec.ts │ │ │ │ ├── nuget.stub.spec.ts │ │ │ │ └── util.ts │ │ │ └── vscode-node/ │ │ │ ├── commands.ts │ │ │ ├── mcpToolCallingLoop.tsx │ │ │ ├── mcpToolCallingLoopPrompt.tsx │ │ │ ├── mcpToolCallingTools.tsx │ │ │ ├── nuget.ts │ │ │ └── util.ts │ │ ├── notebook/ │ │ │ └── vscode-node/ │ │ │ └── followActions.ts │ │ ├── onboardDebug/ │ │ │ ├── common/ │ │ │ │ └── launchConfigService.ts │ │ │ ├── node/ │ │ │ │ ├── commandToConfigConverter.tsx │ │ │ │ ├── copilotDebugCommandSessionFactory.tsx │ │ │ │ ├── copilotDebugWorker/ │ │ │ │ │ ├── copilotDebugWorker.ps1 │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── open.ts │ │ │ │ │ ├── rpc.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ └── streamSplitter.ts │ │ │ │ ├── debuggableCommandIdentifier.ts │ │ │ │ ├── languageToolsProvider.tsx │ │ │ │ └── parseLaunchConfigFromResponse.ts │ │ │ ├── test/ │ │ │ │ └── node/ │ │ │ │ ├── debuggableCommandIdentifier.spec.ts │ │ │ │ └── parseLaunchConfigFromResponse.spec.ts │ │ │ ├── vscode/ │ │ │ │ └── launchConfigService.ts │ │ │ └── vscode-node/ │ │ │ ├── copilotDebugCommandContribution.ts │ │ │ ├── copilotDebugCommandHandle.ts │ │ │ ├── copilotDebugCommandSession.ts │ │ │ └── onboardTerminalTestsContribution.ts │ │ ├── otel/ │ │ │ └── vscode-node/ │ │ │ └── otelContrib.ts │ │ ├── power/ │ │ │ ├── common/ │ │ │ │ └── powerService.ts │ │ │ └── vscode-node/ │ │ │ ├── powerService.ts │ │ │ └── powerStateLogger.ts │ │ ├── prompt/ │ │ │ ├── common/ │ │ │ │ ├── chatVariablesCollection.ts │ │ │ │ ├── codeGuesser.ts │ │ │ │ ├── conversation.ts │ │ │ │ ├── fileTreeParser.ts │ │ │ │ ├── importStatement.ts │ │ │ │ ├── intents.ts │ │ │ │ ├── promptCategorizationTaxonomy.ts │ │ │ │ ├── repository.ts │ │ │ │ ├── specialRequestTypes.ts │ │ │ │ ├── streamingGrammar.ts │ │ │ │ └── toolCallRound.ts │ │ │ ├── node/ │ │ │ │ ├── chatMLFetcher.ts │ │ │ │ ├── chatMLFetcherTelemetry.ts │ │ │ │ ├── chatParticipantRequestHandler.ts │ │ │ │ ├── chatParticipantTelemetry.ts │ │ │ │ ├── codebaseToolCalling.ts │ │ │ │ ├── conversation.ts │ │ │ │ ├── defaultIntentRequestHandler.ts │ │ │ │ ├── definitionAroundCursor.tsx │ │ │ │ ├── devContainerConfigGenerator.ts │ │ │ │ ├── documentContext.ts │ │ │ │ ├── editFromDiffGeneration.ts │ │ │ │ ├── editGeneration.ts │ │ │ │ ├── executionSubagentToolCallingLoop.ts │ │ │ │ ├── feedbackGenerator.ts │ │ │ │ ├── feedbackReporter.ts │ │ │ │ ├── gitCommitMessageGenerator.ts │ │ │ │ ├── githubPullRequestTitleAndDescriptionGenerator.ts │ │ │ │ ├── indentationGuesser.ts │ │ │ │ ├── intentDetector.tsx │ │ │ │ ├── intentRegistry.ts │ │ │ │ ├── intents.ts │ │ │ │ ├── promptCategorizer.ts │ │ │ │ ├── promptVariablesService.ts │ │ │ │ ├── pseudoStartStopConversationCallback.ts │ │ │ │ ├── repoInfoTelemetry.ts │ │ │ │ ├── responseProcessorContext.ts │ │ │ │ ├── searchSubagentToolCallingLoop.ts │ │ │ │ ├── settingsEditorSearchResultsSelector.ts │ │ │ │ ├── streamingEdits.ts │ │ │ │ ├── summarizer.ts │ │ │ │ ├── telemetry.ts │ │ │ │ ├── test/ │ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ │ └── defaultIntentRequestHandler.spec.ts.snap │ │ │ │ │ ├── chatMLFetcherResponseApiTelemetry.spec.ts │ │ │ │ │ ├── chatMLFetcherRetry.spec.ts │ │ │ │ │ ├── codeGuesser.spec.ts │ │ │ │ │ ├── defaultIntentRequestHandler.spec.ts │ │ │ │ │ ├── feedbackGenerator.spec.ts │ │ │ │ │ ├── indentationGuesser.spec.ts │ │ │ │ │ ├── positionOffsetTransformer.spec.ts │ │ │ │ │ ├── repoInfoTelemetry.spec.ts │ │ │ │ │ ├── streamingEdits.spec.ts │ │ │ │ │ └── testFiles.spec.ts │ │ │ │ ├── test2Impl.tsx │ │ │ │ ├── testExample.tsx │ │ │ │ ├── testFiles.ts │ │ │ │ ├── title.ts │ │ │ │ └── todoListContextProvider.ts │ │ │ ├── test/ │ │ │ │ ├── common/ │ │ │ │ │ ├── fileTreeParser.spec.ts │ │ │ │ │ └── streamingGrammar.spec.ts │ │ │ │ └── node/ │ │ │ │ └── conversation.spec.ts │ │ │ └── vscode-node/ │ │ │ ├── debugCommands.ts │ │ │ ├── devContainerConfigurationServiceImpl.ts │ │ │ ├── endpointProviderImpl.ts │ │ │ ├── gitCommitMessageServiceImpl.ts │ │ │ ├── gitDiffService.ts │ │ │ ├── promptVariablesService.ts │ │ │ ├── renameSuggestions.ts │ │ │ ├── requestLoggerImpl.ts │ │ │ ├── requestLoggerToolResult.tsx │ │ │ ├── scenarioAutomationEndpointProviderImpl.ts │ │ │ ├── settingsEditorSearchServiceImpl.ts │ │ │ ├── test/ │ │ │ │ ├── gitDiffService.spec.ts │ │ │ │ └── promptVariablesService.spec.ts │ │ │ └── workspaceEditRecorder.ts │ │ ├── promptFileContext/ │ │ │ └── vscode-node/ │ │ │ └── promptFileContextService.ts │ │ ├── prompts/ │ │ │ ├── common/ │ │ │ │ └── chatDiskSessionResources.ts │ │ │ └── node/ │ │ │ ├── agent/ │ │ │ │ ├── agentConversationHistory.tsx │ │ │ │ ├── agentPrompt.tsx │ │ │ │ ├── allAgentPrompts.ts │ │ │ │ ├── anthropicPrompts.tsx │ │ │ │ ├── backgroundSummarizer.ts │ │ │ │ ├── copilotCLIPrompt.tsx │ │ │ │ ├── defaultAgentInstructions.tsx │ │ │ │ ├── executionSubagentPrompt.tsx │ │ │ │ ├── fileLinkificationInstructions.tsx │ │ │ │ ├── geminiPrompts.tsx │ │ │ │ ├── minimaxPrompts.tsx │ │ │ │ ├── openai/ │ │ │ │ │ ├── defaultOpenAIPrompt.tsx │ │ │ │ │ ├── gpt51CodexPrompt.tsx │ │ │ │ │ ├── gpt51Prompt.tsx │ │ │ │ │ ├── gpt52Prompt.tsx │ │ │ │ │ ├── gpt53CodexPrompt.tsx │ │ │ │ │ ├── gpt54Prompt.tsx │ │ │ │ │ ├── gpt5CodexPrompt.tsx │ │ │ │ │ └── gpt5Prompt.tsx │ │ │ │ ├── promptRegistry.ts │ │ │ │ ├── searchSubagentPrompt.tsx │ │ │ │ ├── simpleSummarizedHistoryPrompt.tsx │ │ │ │ ├── summarizedConversationHistory.tsx │ │ │ │ ├── test/ │ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ │ ├── agentPrompts-arctic-fox/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-tool_use-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools.spec.snap │ │ │ │ │ │ │ ├── all_tools-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── all_tools.spec.snap │ │ │ │ │ │ │ ├── cache_BPs-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── cache_BPs.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions.spec.snap │ │ │ │ │ │ │ ├── one_attachment-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── one_attachment.spec.snap │ │ │ │ │ │ │ ├── simple_case-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── simple_case.spec.snap │ │ │ │ │ │ │ ├── tool_use-arctic-fox.spec.snap │ │ │ │ │ │ │ └── tool_use.spec.snap │ │ │ │ │ │ ├── agentPrompts-claude-haiku-4.5/ │ │ │ │ │ │ │ ├── all_non_edit_tools.spec.snap │ │ │ │ │ │ │ ├── all_tools.spec.snap │ │ │ │ │ │ │ ├── cache_BPs.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions.spec.snap │ │ │ │ │ │ │ ├── one_attachment.spec.snap │ │ │ │ │ │ │ ├── simple_case.spec.snap │ │ │ │ │ │ │ └── tool_use.spec.snap │ │ │ │ │ │ ├── agentPrompts-claude-opus-4.5/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-tool_use-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools.spec.snap │ │ │ │ │ │ │ ├── all_tools-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── all_tools.spec.snap │ │ │ │ │ │ │ ├── cache_BPs-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── cache_BPs.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions.spec.snap │ │ │ │ │ │ │ ├── one_attachment-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── one_attachment.spec.snap │ │ │ │ │ │ │ ├── simple_case-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── simple_case.spec.snap │ │ │ │ │ │ │ ├── tool_use-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ └── tool_use.spec.snap │ │ │ │ │ │ ├── agentPrompts-claude-opus-4.6/ │ │ │ │ │ │ │ ├── all_non_edit_tools.spec.snap │ │ │ │ │ │ │ ├── all_tools.spec.snap │ │ │ │ │ │ │ ├── cache_BPs.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions.spec.snap │ │ │ │ │ │ │ ├── one_attachment.spec.snap │ │ │ │ │ │ │ ├── simple_case.spec.snap │ │ │ │ │ │ │ └── tool_use.spec.snap │ │ │ │ │ │ ├── agentPrompts-claude-opus-4.6-fast/ │ │ │ │ │ │ │ ├── all_non_edit_tools.spec.snap │ │ │ │ │ │ │ ├── all_tools.spec.snap │ │ │ │ │ │ │ ├── cache_BPs.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions.spec.snap │ │ │ │ │ │ │ ├── one_attachment.spec.snap │ │ │ │ │ │ │ ├── simple_case.spec.snap │ │ │ │ │ │ │ └── tool_use.spec.snap │ │ │ │ │ │ ├── agentPrompts-claude-sonnet-4.5/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-tool_use-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools.spec.snap │ │ │ │ │ │ │ ├── all_tools-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── all_tools.spec.snap │ │ │ │ │ │ │ ├── cache_BPs-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── cache_BPs.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions.spec.snap │ │ │ │ │ │ │ ├── one_attachment-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── one_attachment.spec.snap │ │ │ │ │ │ │ ├── simple_case-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── simple_case.spec.snap │ │ │ │ │ │ │ ├── tool_use-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ └── tool_use.spec.snap │ │ │ │ │ │ ├── agentPrompts-default/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-default.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-default.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-default.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-default.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-default.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-default.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-default.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-default.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-default.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-tool_use-default.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools-default.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools.spec.snap │ │ │ │ │ │ │ ├── all_tools-default.spec.snap │ │ │ │ │ │ │ ├── all_tools.spec.snap │ │ │ │ │ │ │ ├── cache_BPs-default.spec.snap │ │ │ │ │ │ │ ├── cache_BPs.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round-default.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message-default.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind-default.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions-default.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions.spec.snap │ │ │ │ │ │ │ ├── one_attachment-default.spec.snap │ │ │ │ │ │ │ ├── one_attachment.spec.snap │ │ │ │ │ │ │ ├── simple_case-default.spec.snap │ │ │ │ │ │ │ ├── simple_case.spec.snap │ │ │ │ │ │ │ ├── tool_use-default.spec.snap │ │ │ │ │ │ │ └── tool_use.spec.snap │ │ │ │ │ │ ├── agentPrompts-gemini-2.0-flash/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-tool_use-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools.spec.snap │ │ │ │ │ │ │ ├── all_tools-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── all_tools.spec.snap │ │ │ │ │ │ │ ├── cache_BPs-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── cache_BPs.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions.spec.snap │ │ │ │ │ │ │ ├── one_attachment-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── one_attachment.spec.snap │ │ │ │ │ │ │ ├── simple_case-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── simple_case.spec.snap │ │ │ │ │ │ │ ├── tool_use-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ └── tool_use.spec.snap │ │ │ │ │ │ ├── agentPrompts-gpt-4.1/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-tool_use-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools.spec.snap │ │ │ │ │ │ │ ├── all_tools-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── all_tools.spec.snap │ │ │ │ │ │ │ ├── cache_BPs-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── cache_BPs.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions.spec.snap │ │ │ │ │ │ │ ├── one_attachment-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── one_attachment.spec.snap │ │ │ │ │ │ │ ├── simple_case-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── simple_case.spec.snap │ │ │ │ │ │ │ ├── tool_use-gpt-4.1.spec.snap │ │ │ │ │ │ │ └── tool_use.spec.snap │ │ │ │ │ │ ├── agentPrompts-gpt-5/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-gpt-5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-gpt-5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-gpt-5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-gpt-5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-gpt-5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-gpt-5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-gpt-5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-gpt-5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-gpt-5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-tool_use-gpt-5.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools-gpt-5.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools.spec.snap │ │ │ │ │ │ │ ├── all_tools-gpt-5.spec.snap │ │ │ │ │ │ │ ├── all_tools.spec.snap │ │ │ │ │ │ │ ├── cache_BPs-gpt-5.spec.snap │ │ │ │ │ │ │ ├── cache_BPs.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round-gpt-5.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message-gpt-5.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind-gpt-5.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions-gpt-5.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions.spec.snap │ │ │ │ │ │ │ ├── one_attachment-gpt-5.spec.snap │ │ │ │ │ │ │ ├── one_attachment.spec.snap │ │ │ │ │ │ │ ├── simple_case-gpt-5.spec.snap │ │ │ │ │ │ │ ├── simple_case.spec.snap │ │ │ │ │ │ │ ├── tool_use-gpt-5.spec.snap │ │ │ │ │ │ │ └── tool_use.spec.snap │ │ │ │ │ │ ├── agentPrompts-gpt-5-codex/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-tool_use-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools.spec.snap │ │ │ │ │ │ │ ├── all_tools-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── all_tools.spec.snap │ │ │ │ │ │ │ ├── cache_BPs-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── cache_BPs.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions.spec.snap │ │ │ │ │ │ │ ├── one_attachment-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── one_attachment.spec.snap │ │ │ │ │ │ │ ├── simple_case-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── simple_case.spec.snap │ │ │ │ │ │ │ ├── tool_use-gpt-5-codex.spec.snap │ │ │ │ │ │ │ └── tool_use.spec.snap │ │ │ │ │ │ ├── agentPrompts-gpt-5-mini/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-tool_use-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools.spec.snap │ │ │ │ │ │ │ ├── all_tools-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── all_tools.spec.snap │ │ │ │ │ │ │ ├── cache_BPs-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── cache_BPs.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions.spec.snap │ │ │ │ │ │ │ ├── one_attachment-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── one_attachment.spec.snap │ │ │ │ │ │ │ ├── simple_case-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── simple_case.spec.snap │ │ │ │ │ │ │ ├── tool_use-gpt-5-mini.spec.snap │ │ │ │ │ │ │ └── tool_use.spec.snap │ │ │ │ │ │ ├── agentPrompts-gpt-5.1/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-tool_use-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools.spec.snap │ │ │ │ │ │ │ ├── all_tools-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── all_tools.spec.snap │ │ │ │ │ │ │ ├── cache_BPs-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── cache_BPs.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions.spec.snap │ │ │ │ │ │ │ ├── one_attachment-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── one_attachment.spec.snap │ │ │ │ │ │ │ ├── simple_case-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── simple_case.spec.snap │ │ │ │ │ │ │ ├── tool_use-gpt-5.1.spec.snap │ │ │ │ │ │ │ └── tool_use.spec.snap │ │ │ │ │ │ ├── agentPrompts-gpt-5.1-codex/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-tool_use-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools.spec.snap │ │ │ │ │ │ │ ├── all_tools-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── all_tools.spec.snap │ │ │ │ │ │ │ ├── cache_BPs-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── cache_BPs.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions.spec.snap │ │ │ │ │ │ │ ├── one_attachment-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── one_attachment.spec.snap │ │ │ │ │ │ │ ├── simple_case-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── simple_case.spec.snap │ │ │ │ │ │ │ ├── tool_use-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ └── tool_use.spec.snap │ │ │ │ │ │ ├── agentPrompts-gpt-5.1-codex-mini/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-tool_use-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools.spec.snap │ │ │ │ │ │ │ ├── all_tools-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── all_tools.spec.snap │ │ │ │ │ │ │ ├── cache_BPs-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── cache_BPs.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions.spec.snap │ │ │ │ │ │ │ ├── one_attachment-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── one_attachment.spec.snap │ │ │ │ │ │ │ ├── simple_case-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── simple_case.spec.snap │ │ │ │ │ │ │ ├── tool_use-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ └── tool_use.spec.snap │ │ │ │ │ │ ├── agentPrompts-grok-code-fast-1/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-tool_use-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── all_non_edit_tools.spec.snap │ │ │ │ │ │ │ ├── all_tools-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── all_tools.spec.snap │ │ │ │ │ │ │ ├── cache_BPs-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── cache_BPs.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── cache_BPs_multi_round.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── custom_instructions_not_in_system_message.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── edited_file_events_grouped_by_kind.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── omit_base_agent_instructions.spec.snap │ │ │ │ │ │ │ ├── one_attachment-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── one_attachment.spec.snap │ │ │ │ │ │ │ ├── simple_case-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── simple_case.spec.snap │ │ │ │ │ │ │ ├── tool_use-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ └── tool_use.spec.snap │ │ │ │ │ │ ├── arctic-fox/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-arctic-fox.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-arctic-fox.spec.snap │ │ │ │ │ │ │ └── agentPrompts-tool_use-arctic-fox.spec.snap │ │ │ │ │ │ ├── claude-opus-4.5/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-claude-opus-4.5.spec.snap │ │ │ │ │ │ │ └── agentPrompts-tool_use-claude-opus-4.5.spec.snap │ │ │ │ │ │ ├── claude-sonnet-4.5/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ │ └── agentPrompts-tool_use-claude-sonnet-4.5.spec.snap │ │ │ │ │ │ ├── default/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-default.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-default.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-default.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-default.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-default.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-default.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-default.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-default.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-default.spec.snap │ │ │ │ │ │ │ └── agentPrompts-tool_use-default.spec.snap │ │ │ │ │ │ ├── gemini-2.0-flash/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-gemini-2.0-flash.spec.snap │ │ │ │ │ │ │ └── agentPrompts-tool_use-gemini-2.0-flash.spec.snap │ │ │ │ │ │ ├── gpt-4.1/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-gpt-4.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-gpt-4.1.spec.snap │ │ │ │ │ │ │ └── agentPrompts-tool_use-gpt-4.1.spec.snap │ │ │ │ │ │ ├── gpt-5/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-gpt-5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-gpt-5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-gpt-5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-gpt-5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-gpt-5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-gpt-5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-gpt-5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-gpt-5.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-gpt-5.spec.snap │ │ │ │ │ │ │ └── agentPrompts-tool_use-gpt-5.spec.snap │ │ │ │ │ │ ├── gpt-5-codex/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-gpt-5-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-gpt-5-codex.spec.snap │ │ │ │ │ │ │ └── agentPrompts-tool_use-gpt-5-codex.spec.snap │ │ │ │ │ │ ├── gpt-5-mini/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-gpt-5-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-gpt-5-mini.spec.snap │ │ │ │ │ │ │ └── agentPrompts-tool_use-gpt-5-mini.spec.snap │ │ │ │ │ │ ├── gpt-5.1/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-gpt-5.1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-gpt-5.1.spec.snap │ │ │ │ │ │ │ └── agentPrompts-tool_use-gpt-5.1.spec.snap │ │ │ │ │ │ ├── gpt-5.1-codex/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-gpt-5.1-codex.spec.snap │ │ │ │ │ │ │ └── agentPrompts-tool_use-gpt-5.1-codex.spec.snap │ │ │ │ │ │ ├── gpt-5.1-codex-mini/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ │ └── agentPrompts-tool_use-gpt-5.1-codex-mini.spec.snap │ │ │ │ │ │ ├── grok-code-fast-1/ │ │ │ │ │ │ │ ├── agentPrompts-all_non_edit_tools-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-all_tools-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-cache_BPs_multi_round-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-custom_instructions_not_in_system_message-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-edited_file_events_grouped_by_kind-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-omit_base_agent_instructions-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-one_attachment-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ ├── agentPrompts-simple_case-grok-code-fast-1.spec.snap │ │ │ │ │ │ │ └── agentPrompts-tool_use-grok-code-fast-1.spec.snap │ │ │ │ │ │ ├── parseAttachments.spec.ts.snap │ │ │ │ │ │ ├── summarization-currentTurn-Agent.spec.snap │ │ │ │ │ │ ├── summarization-currentTurnEarlierRound-Agent.spec.snap │ │ │ │ │ │ ├── summarization-currentTurnEarlierRound-FullSumm.spec.snap │ │ │ │ │ │ ├── summarization-currentTurnEarlierRound-SimpleSummarizedHistory.spec.snap │ │ │ │ │ │ ├── summarization-duringToolCalling-Agent.spec.snap │ │ │ │ │ │ ├── summarization-duringToolCalling-FullSumm.spec.snap │ │ │ │ │ │ ├── summarization-duringToolCalling-SimpleSummarizedHistory.spec.snap │ │ │ │ │ │ ├── summarization-previousTurnMultiple-Agent.spec.snap │ │ │ │ │ │ ├── summarization-previousTurnMultiple-FullSumm.spec.snap │ │ │ │ │ │ ├── summarization-previousTurnMultiple-SimpleSummarizedHistory.spec.snap │ │ │ │ │ │ ├── summarization-previousTurnNoRounds-Agent.spec.snap │ │ │ │ │ │ ├── summarization-previousTurnNoRounds-FullSumm.spec.snap │ │ │ │ │ │ └── summarization-previousTurnNoRounds-SimpleSummarizedHistory.spec.snap │ │ │ │ │ ├── agentPrompt.spec.tsx │ │ │ │ │ ├── agentTasksInstructions.spec.tsx │ │ │ │ │ ├── backgroundSummarizer.spec.ts │ │ │ │ │ ├── parseAttachments.spec.ts │ │ │ │ │ ├── summarization.spec.tsx │ │ │ │ │ └── terminalPrompt.spec.tsx │ │ │ │ ├── vscModelPrompts.tsx │ │ │ │ ├── xAIPrompts.tsx │ │ │ │ └── zaiPrompts.tsx │ │ │ ├── base/ │ │ │ │ ├── capabilities.tsx │ │ │ │ ├── common.tsx │ │ │ │ ├── copilotIdentity.tsx │ │ │ │ ├── instructionMessage.tsx │ │ │ │ ├── promptElement.ts │ │ │ │ ├── promptRenderer.ts │ │ │ │ ├── responseTranslationRules.tsx │ │ │ │ ├── safetyRules.tsx │ │ │ │ ├── tag.tsx │ │ │ │ └── terminalState.tsx │ │ │ ├── chatDiskSessionResourcesImpl.ts │ │ │ ├── codeMapper/ │ │ │ │ ├── codeMapper.ts │ │ │ │ ├── codeMapperPrompt.tsx │ │ │ │ ├── codeMapperService.ts │ │ │ │ └── patchEditGeneration.tsx │ │ │ ├── devcontainer/ │ │ │ │ └── devContainerConfigPrompt.tsx │ │ │ ├── feedback/ │ │ │ │ ├── currentChange.tsx │ │ │ │ └── provideFeedback.tsx │ │ │ ├── git/ │ │ │ │ ├── gitChanges.tsx │ │ │ │ └── gitCommitMessagePrompt.tsx │ │ │ ├── github/ │ │ │ │ └── pullRequestDescriptionPrompt.tsx │ │ │ ├── inline/ │ │ │ │ ├── adjustSelection.ts │ │ │ │ ├── diagnosticsContext.tsx │ │ │ │ ├── diffEditGeneration.tsx │ │ │ │ ├── fixCookbookService.ts │ │ │ │ ├── inlineChat2Prompt.tsx │ │ │ │ ├── inlineChatEditCodePrompt.tsx │ │ │ │ ├── inlineChatEditMarkdownPrompt.tsx │ │ │ │ ├── inlineChatFix3Prompt.tsx │ │ │ │ ├── inlineChatGenerateCodePrompt.tsx │ │ │ │ ├── inlineChatGenerateMarkdownPrompt.tsx │ │ │ │ ├── inlineChatNotebookCommon.ts │ │ │ │ ├── inlineChatNotebookCommonPromptElements.tsx │ │ │ │ ├── inlineChatNotebookEditPrompt.tsx │ │ │ │ ├── inlineChatNotebookFixPrompt.tsx │ │ │ │ ├── inlineChatNotebookGeneratePrompt.tsx │ │ │ │ ├── inlineChatWorkspaceSearch.tsx │ │ │ │ ├── languageServerContextPrompt.tsx │ │ │ │ ├── progressMessages.tsx │ │ │ │ ├── promptingSummarizedDocument.ts │ │ │ │ ├── pythonCookbookData.ts │ │ │ │ ├── summarizedDocument/ │ │ │ │ │ ├── fragments.ts │ │ │ │ │ ├── implementation.ts │ │ │ │ │ ├── projectedText.ts │ │ │ │ │ ├── summarizeDocument.ts │ │ │ │ │ └── summarizeDocumentHelpers.ts │ │ │ │ ├── test/ │ │ │ │ │ └── inlineChat2Prompt.spec.tsx │ │ │ │ ├── utils/ │ │ │ │ │ └── streaming.ts │ │ │ │ ├── visualization.ts │ │ │ │ └── workingCopies.ts │ │ │ ├── notebook/ │ │ │ │ └── commonPrompts.tsx │ │ │ ├── panel/ │ │ │ │ ├── binaryFileHexdump.tsx │ │ │ │ ├── chatVariables.tsx │ │ │ │ ├── codeBlockFormattingRules.tsx │ │ │ │ ├── codebaseAgentPrompt.tsx │ │ │ │ ├── conversationHistory.tsx │ │ │ │ ├── currentEditor.tsx │ │ │ │ ├── currentSelection.tsx │ │ │ │ ├── customInstructions.tsx │ │ │ │ ├── definitionAtPosition.tsx │ │ │ │ ├── editCodePrompt.tsx │ │ │ │ ├── editCodePrompt2.tsx │ │ │ │ ├── editorIntegrationRules.tsx │ │ │ │ ├── explain.tsx │ │ │ │ ├── fileVariable.tsx │ │ │ │ ├── image.tsx │ │ │ │ ├── newNotebook.tsx │ │ │ │ ├── newWorkspace/ │ │ │ │ │ ├── newWorkspace.tsx │ │ │ │ │ └── newWorkspaceContents.tsx │ │ │ │ ├── notebookEditCodePrompt.tsx │ │ │ │ ├── notebookInlinePrompt.tsx │ │ │ │ ├── notebookSummaryChangePrompt.tsx │ │ │ │ ├── notebookVariables.tsx │ │ │ │ ├── panelChatBasePrompt.tsx │ │ │ │ ├── panelChatFixPrompt.tsx │ │ │ │ ├── preferences.tsx │ │ │ │ ├── projectLabels.tsx │ │ │ │ ├── promptCategorization.tsx │ │ │ │ ├── promptFile.tsx │ │ │ │ ├── referencesAtPosition.tsx │ │ │ │ ├── safeElements.tsx │ │ │ │ ├── search.tsx │ │ │ │ ├── searchPanelKeywordsPrompt.tsx │ │ │ │ ├── searchPanelPrompt.tsx │ │ │ │ ├── startDebugging.tsx │ │ │ │ ├── symbolAtCursor.tsx │ │ │ │ ├── symbolDefinitions.tsx │ │ │ │ ├── terminal.tsx │ │ │ │ ├── terminalExplain.tsx │ │ │ │ ├── terminalLastCommand.tsx │ │ │ │ ├── terminalQuickFix.tsx │ │ │ │ ├── terminalSelection.tsx │ │ │ │ ├── test/ │ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ │ └── fileVariable.spec.ts.snap │ │ │ │ │ ├── chatVariablesHelpers.spec.ts │ │ │ │ │ ├── fileVariable.spec.ts │ │ │ │ │ └── toolCalling.spec.ts │ │ │ │ ├── title.tsx │ │ │ │ ├── toolCalling.tsx │ │ │ │ ├── unsafeElements.tsx │ │ │ │ ├── vscode.tsx │ │ │ │ └── workspace/ │ │ │ │ ├── metaPrompt.tsx │ │ │ │ ├── test/ │ │ │ │ │ └── visualFileTree.spec.ts │ │ │ │ ├── visualFileTree.ts │ │ │ │ ├── workspaceContext.tsx │ │ │ │ ├── workspaceFoldersHint.tsx │ │ │ │ └── workspaceStructure.tsx │ │ │ ├── settingsEditor/ │ │ │ │ └── settingsEditorSuggestQueryPrompt.tsx │ │ │ └── test/ │ │ │ ├── adjustSelection.spec.ts │ │ │ ├── chatDiskSessionResources.spec.ts │ │ │ ├── fixtures/ │ │ │ │ ├── 5710.selection.ts │ │ │ │ ├── 5710.summarized.ts │ │ │ │ ├── 5710.ts │ │ │ │ ├── BasketService.cs │ │ │ │ ├── BasketService.selection.cs │ │ │ │ ├── BasketService.summarized.cs │ │ │ │ ├── EditForm.selection.tsx │ │ │ │ ├── EditForm.summarized.tsx │ │ │ │ ├── EditForm.tsx │ │ │ │ ├── bracketPairsTree.summarized.ts │ │ │ │ ├── bracketPairsTree.ts │ │ │ │ ├── codeEditorWidget.1.summarized.ts │ │ │ │ ├── codeEditorWidget.2.summarized.ts │ │ │ │ ├── codeEditorWidget.3.summarized.ts │ │ │ │ ├── codeEditorWidget.ts │ │ │ │ ├── codeEditorWidget.ts.1.tempo-summarized │ │ │ │ ├── cppNoExtraSemicolons.cpp │ │ │ │ ├── cppNoExtraSemicolons.summarized.cpp │ │ │ │ ├── editorGroupWatermark.summarized.ts │ │ │ │ ├── editorGroupWatermark.ts │ │ │ │ ├── editorGroupWatermark.ts.summarized.round1 │ │ │ │ ├── editorGroupWatermark.ts.summarized.round2 │ │ │ │ ├── extHost.api.impl.selection.ts │ │ │ │ ├── extHost.api.impl.summarized.ts │ │ │ │ ├── extHost.api.impl.ts │ │ │ │ ├── keybindingParser.summarized.ts │ │ │ │ ├── keybindingParser.ts │ │ │ │ ├── map.summarized.ts │ │ │ │ ├── map.summarized.ts.view-port │ │ │ │ ├── map.ts │ │ │ │ ├── problem1.cpp │ │ │ │ ├── problem1.summarized.cpp │ │ │ │ ├── problem2.cpp │ │ │ │ ├── problem2.summarized.cpp │ │ │ │ ├── pseudoStartStopConversationCallbackTest.selection.ts │ │ │ │ ├── pseudoStartStopConversationCallbackTest.summarized.ts │ │ │ │ ├── pseudoStartStopConversationCallbackTest.ts │ │ │ │ ├── pullRequestModel.selection.ts │ │ │ │ ├── pullRequestModel.summarized.ts │ │ │ │ ├── pullRequestModel.ts │ │ │ │ ├── simpleClass.summarized.tsx │ │ │ │ ├── simpleClass.tsx │ │ │ │ ├── strings.test-example.2.summarized.ts │ │ │ │ ├── strings.test-example.3.summarized.ts │ │ │ │ ├── strings.test-example.summarized.ts │ │ │ │ ├── strings.test-example.summarized.ts.round2 │ │ │ │ ├── strings.test-example.ts │ │ │ │ ├── strings.test-example.ts.summarized.round1 │ │ │ │ ├── strings.test-example.ts.summarized.round2 │ │ │ │ ├── tempo-actions.html │ │ │ │ ├── tempo-actions.html.3.tempo-summarized │ │ │ │ ├── tempo-actions.ts │ │ │ │ ├── tempo-actions.ts.2.tempo-summarized │ │ │ │ ├── tempo-actions.ts.3.tempo-summarized │ │ │ │ ├── tempo-chatActions.ts │ │ │ │ ├── tempo-chatActions.ts.2.tempo-summarized │ │ │ │ ├── tempo-chatContextActions.ts │ │ │ │ ├── tempo-chatContextActions.ts.2.tempo-summarized │ │ │ │ ├── view.css │ │ │ │ ├── view.summarized.css │ │ │ │ ├── vscode.proposed.chatParticipantAdditions.d.selection.ts │ │ │ │ ├── vscode.proposed.chatParticipantAdditions.d.summarized.ts │ │ │ │ ├── vscode.proposed.chatParticipantAdditions.d.ts │ │ │ │ ├── webview-index.selection.ts │ │ │ │ ├── webview-index.summarized.ts │ │ │ │ ├── webview-index.ts │ │ │ │ ├── workbench-dev.html │ │ │ │ ├── workbench-dev.selection.html │ │ │ │ └── workbench-dev.summarized.html │ │ │ ├── projectedText.spec.ts │ │ │ ├── summarizeDocument.spec.ts │ │ │ ├── summarizeDocumentPlayground.ts │ │ │ ├── utils.ts │ │ │ └── workingCopies.spec.ts │ │ ├── renameSuggestions/ │ │ │ ├── common/ │ │ │ │ └── namingConvention.ts │ │ │ ├── node/ │ │ │ │ ├── renameSuggestionsPrompt.tsx │ │ │ │ └── renameSuggestionsProvider.ts │ │ │ └── test/ │ │ │ ├── common/ │ │ │ │ └── namingConvention.spec.ts │ │ │ └── node/ │ │ │ └── renameSuggestionsProvider.spec.tsx │ │ ├── replay/ │ │ │ ├── common/ │ │ │ │ ├── chatReplayResponses.ts │ │ │ │ └── chatReplayTypes.ts │ │ │ ├── node/ │ │ │ │ ├── chatReplayExport.ts │ │ │ │ ├── replayParser.ts │ │ │ │ ├── replayParsing.spec.ts │ │ │ │ └── spec.chatreplay.json │ │ │ └── vscode-node/ │ │ │ ├── chatReplayContrib.ts │ │ │ ├── chatReplayNotebookSerializer.ts │ │ │ ├── chatReplayParticipant.ts │ │ │ ├── chatReplaySessionProvider.ts │ │ │ ├── replayDebugSession.ts │ │ │ └── test/ │ │ │ └── chatReplayNotebook.spec.ts │ │ ├── review/ │ │ │ └── node/ │ │ │ ├── doReview.ts │ │ │ ├── githubPullRequestReviewerCommentsProvider.ts │ │ │ ├── githubReviewAgent.ts │ │ │ └── test/ │ │ │ ├── doReview.spec.ts │ │ │ ├── githubReviewAgent.spec.ts │ │ │ └── reviewCommand.spec.ts │ │ ├── search/ │ │ │ └── vscode-node/ │ │ │ └── commands.ts │ │ ├── settingsSchema/ │ │ │ └── vscode-node/ │ │ │ └── settingsSchemaFeature.ts │ │ ├── survey/ │ │ │ └── vscode-node/ │ │ │ └── surveyCommands.ts │ │ ├── telemetry/ │ │ │ ├── common/ │ │ │ │ └── lifecycleTelemetryContrib.ts │ │ │ └── vscode/ │ │ │ └── githubTelemetryForwardingContrib.ts │ │ ├── test/ │ │ │ ├── common/ │ │ │ │ └── importRewriting.spec.ts │ │ │ ├── node/ │ │ │ │ ├── configurations.spec.ts │ │ │ │ ├── editFromDiffGeneration.spec.ts │ │ │ │ ├── extractCodeSnippets.spec.ts │ │ │ │ ├── fixtures/ │ │ │ │ │ ├── gitdiff/ │ │ │ │ │ │ ├── 01-basic │ │ │ │ │ │ ├── 01-basic-add-2-lines │ │ │ │ │ │ ├── 01-basic-add-2-lines.diff │ │ │ │ │ │ ├── 01-basic-add-first-line │ │ │ │ │ │ ├── 01-basic-add-first-line.diff │ │ │ │ │ │ ├── 01-basic-add-last-line │ │ │ │ │ │ ├── 01-basic-add-last-line-with-eol │ │ │ │ │ │ ├── 01-basic-add-last-line-with-eol.diff │ │ │ │ │ │ ├── 01-basic-add-last-line.diff │ │ │ │ │ │ ├── 01-basic-add-line │ │ │ │ │ │ ├── 01-basic-add-line.diff │ │ │ │ │ │ ├── 01-basic-move-lines │ │ │ │ │ │ ├── 01-basic-move-lines.diff │ │ │ │ │ │ ├── 01-basic-remove-first-line │ │ │ │ │ │ ├── 01-basic-remove-first-line.diff │ │ │ │ │ │ ├── 01-basic-remove-last-line │ │ │ │ │ │ ├── 01-basic-remove-last-line-with-eol │ │ │ │ │ │ ├── 01-basic-remove-last-line-with-eol.diff │ │ │ │ │ │ ├── 01-basic-remove-last-line.diff │ │ │ │ │ │ ├── 01-basic-remove-line │ │ │ │ │ │ ├── 01-basic-remove-line.diff │ │ │ │ │ │ ├── 01-basic-replace-line │ │ │ │ │ │ ├── 01-basic-replace-line.diff │ │ │ │ │ │ ├── 02-basicWithEol │ │ │ │ │ │ ├── 02-basicWithEol-add-line │ │ │ │ │ │ ├── 02-basicWithEol-add-line.diff │ │ │ │ │ │ ├── 02-basicWithEol-remove-eol │ │ │ │ │ │ ├── 02-basicWithEol-remove-eol.diff │ │ │ │ │ │ ├── 02-basicWithEol-remove-last-line │ │ │ │ │ │ ├── 02-basicWithEol-remove-last-line.diff │ │ │ │ │ │ ├── 03-large │ │ │ │ │ │ ├── 03-large-many-changes │ │ │ │ │ │ ├── 03-large-many-changes.diff │ │ │ │ │ │ └── generate-diffs.js │ │ │ │ │ ├── patch/ │ │ │ │ │ │ ├── basic/ │ │ │ │ │ │ │ ├── nested-codeblock.expected.txt │ │ │ │ │ │ │ ├── nested-codeblock.original.txt │ │ │ │ │ │ │ ├── nested-codeblock.patch.txt │ │ │ │ │ │ │ ├── test1.expected.txt │ │ │ │ │ │ │ ├── test1.original.txt │ │ │ │ │ │ │ ├── test1.patch.txt │ │ │ │ │ │ │ ├── two-blocks.expected.txt │ │ │ │ │ │ │ ├── two-blocks.original.txt │ │ │ │ │ │ │ └── two-blocks.patch.txt │ │ │ │ │ │ ├── indentation/ │ │ │ │ │ │ │ ├── aml-10-58-not-defined-01.expected.txt │ │ │ │ │ │ │ ├── aml-10-58-not-defined-01.original.txt │ │ │ │ │ │ │ ├── aml-10-58-not-defined-01.patch.txt │ │ │ │ │ │ │ ├── aml-8-110-not-defined-00.expected.txt │ │ │ │ │ │ │ ├── aml-8-110-not-defined-00.original.txt │ │ │ │ │ │ │ ├── aml-8-110-not-defined-00.patch.txt │ │ │ │ │ │ │ ├── aml-8-73-no-value-for-argument-in-function-call-00.expected.txt │ │ │ │ │ │ │ ├── aml-8-73-no-value-for-argument-in-function-call-00.original.txt │ │ │ │ │ │ │ ├── aml-8-73-no-value-for-argument-in-function-call-00.patch.txt │ │ │ │ │ │ │ ├── code-mapper-panel-6614-2.expected.txt │ │ │ │ │ │ │ ├── code-mapper-panel-6614-2.original.txt │ │ │ │ │ │ │ ├── code-mapper-panel-6614-2.patch.txt │ │ │ │ │ │ │ ├── code-mapper-panel-6614.expected.txt │ │ │ │ │ │ │ ├── code-mapper-panel-6614.original.txt │ │ │ │ │ │ │ ├── code-mapper-panel-6614.patch.txt │ │ │ │ │ │ │ ├── no-duplicate-case-with-cookbook.expected.txt │ │ │ │ │ │ │ ├── no-duplicate-case-with-cookbook.original.txt │ │ │ │ │ │ │ ├── no-duplicate-case-with-cookbook.patch.txt │ │ │ │ │ │ │ ├── unecessary-parenthesis-00.expected.txt │ │ │ │ │ │ │ ├── unecessary-parenthesis-00.original.txt │ │ │ │ │ │ │ └── unecessary-parenthesis-00.patch.txt │ │ │ │ │ │ └── out-20240514-153256/ │ │ │ │ │ │ ├── class-methods-use-this-with-cookbook-00.expected.txt │ │ │ │ │ │ ├── class-methods-use-this-with-cookbook-00.original.txt │ │ │ │ │ │ ├── class-methods-use-this-with-cookbook-00.patch.txt │ │ │ │ │ │ ├── consistent-this-with-cookbook-00.expected.txt │ │ │ │ │ │ ├── consistent-this-with-cookbook-00.original.txt │ │ │ │ │ │ ├── consistent-this-with-cookbook-00.patch.txt │ │ │ │ │ │ ├── constructor-super-with-cookbook-00.expected.txt │ │ │ │ │ │ ├── constructor-super-with-cookbook-00.original.txt │ │ │ │ │ │ ├── constructor-super-with-cookbook-00.patch.txt │ │ │ │ │ │ ├── max-lines-per-function-with-cookbook-00.expected.txt │ │ │ │ │ │ ├── max-lines-per-function-with-cookbook-00.original.txt │ │ │ │ │ │ ├── max-lines-per-function-with-cookbook-00.patch.txt │ │ │ │ │ │ ├── no-dupe-else-if-with-cookbook-00.expected.txt │ │ │ │ │ │ ├── no-dupe-else-if-with-cookbook-00.original.txt │ │ │ │ │ │ ├── no-dupe-else-if-with-cookbook-00.patch.txt │ │ │ │ │ │ ├── no-duplicate-case-with-cookbook-00.expected.txt │ │ │ │ │ │ ├── no-duplicate-case-with-cookbook-00.original.txt │ │ │ │ │ │ ├── no-duplicate-case-with-cookbook-00.patch.txt │ │ │ │ │ │ ├── no-negated-condition-2-with-cookbook-00.expected.txt │ │ │ │ │ │ ├── no-negated-condition-2-with-cookbook-00.original.txt │ │ │ │ │ │ ├── no-negated-condition-2-with-cookbook-00.patch.txt │ │ │ │ │ │ ├── no-negated-condition-with-cookbook-00.expected.txt │ │ │ │ │ │ ├── no-negated-condition-with-cookbook-00.original.txt │ │ │ │ │ │ ├── no-negated-condition-with-cookbook-00.patch.txt │ │ │ │ │ │ ├── no-new-with-cookbook-00.expected.txt │ │ │ │ │ │ ├── no-new-with-cookbook-00.original.txt │ │ │ │ │ │ ├── no-new-with-cookbook-00.patch.txt │ │ │ │ │ │ ├── no-sequences-with-cookbook-00.expected.txt │ │ │ │ │ │ ├── no-sequences-with-cookbook-00.original.txt │ │ │ │ │ │ ├── no-sequences-with-cookbook-00.patch.txt │ │ │ │ │ │ ├── should-not-generate-an-error-for-variables-declared-in-outer-scopes.expected.txt │ │ │ │ │ │ ├── should-not-generate-an-error-for-variables-declared-in-outer-scopes.original.txt │ │ │ │ │ │ └── should-not-generate-an-error-for-variables-declared-in-outer-scopes.patch.txt │ │ │ │ │ └── pseudodiff/ │ │ │ │ │ ├── 01-simple │ │ │ │ │ ├── 01-simple-replace-2-lines │ │ │ │ │ ├── 01-simple-replace-2-lines.diff │ │ │ │ │ ├── 02-filewithtabs │ │ │ │ │ ├── 02-filewithtabs-replace │ │ │ │ │ ├── 02-filewithtabs-replace.diff │ │ │ │ │ ├── 03-unusedimport │ │ │ │ │ ├── 03-unusedimport-addone │ │ │ │ │ ├── 03-unusedimport-addone.diff │ │ │ │ │ ├── 04-spaces │ │ │ │ │ ├── 04-spaces-replace │ │ │ │ │ ├── 04-spaces-replace.diff │ │ │ │ │ ├── 05-beginend │ │ │ │ │ ├── 05-beginend-move │ │ │ │ │ ├── 05-beginend-move.diff │ │ │ │ │ ├── 06-similarline │ │ │ │ │ ├── 06-similarline-comma │ │ │ │ │ ├── 06-similarline-comma.diff │ │ │ │ │ ├── 07-indent1 │ │ │ │ │ ├── 07-indent1-one │ │ │ │ │ ├── 07-indent1-one.diff │ │ │ │ │ ├── 07-indent1-two │ │ │ │ │ ├── 07-indent1-two.diff │ │ │ │ │ ├── 08-modifyunchanged │ │ │ │ │ ├── 08-modifyunchanged-one │ │ │ │ │ ├── 08-modifyunchanged-one.diff │ │ │ │ │ ├── 09-indent2 │ │ │ │ │ ├── 09-indent2-one │ │ │ │ │ ├── 09-indent2-one.diff │ │ │ │ │ ├── 10-test │ │ │ │ │ ├── 10-test-one │ │ │ │ │ ├── 10-test-one.diff │ │ │ │ │ ├── 10-test-one.messages │ │ │ │ │ ├── 11-replaceatend │ │ │ │ │ ├── 11-replaceatend-one │ │ │ │ │ ├── 11-replaceatend-one.diff │ │ │ │ │ ├── 11-replaceatend-one.messages │ │ │ │ │ ├── 12-insertmethod │ │ │ │ │ ├── 12-insertmethod-one │ │ │ │ │ ├── 12-insertmethod-one.diff │ │ │ │ │ ├── 12-insertmethod-one.messages │ │ │ │ │ ├── 12-insertmethod-two │ │ │ │ │ ├── 12-insertmethod-two.diff │ │ │ │ │ ├── 12-insertmethod-two.messages │ │ │ │ │ ├── 13-coroutine │ │ │ │ │ ├── 13-coroutine-one │ │ │ │ │ ├── 13-coroutine-one.diff │ │ │ │ │ ├── 13-coroutine-one.messages │ │ │ │ │ ├── 14-rob │ │ │ │ │ ├── 14-rob-one │ │ │ │ │ └── 14-rob-one.diff │ │ │ │ ├── intent.spec.ts │ │ │ │ ├── notebookPromptRendering.spec.ts │ │ │ │ ├── patchEditGeneration.spec.ts │ │ │ │ ├── pseudoStartStopConversationCallback.spec.ts │ │ │ │ ├── services.ts │ │ │ │ ├── streaming.spec.ts │ │ │ │ ├── summarizedDocumentRendering.spec.tsx │ │ │ │ ├── telemetry.spec.ts │ │ │ │ ├── testHelpers.ts │ │ │ │ ├── utils.fileTree.spec.ts │ │ │ │ └── utils.spec.ts │ │ │ └── vscode-node/ │ │ │ ├── configurations.test.ts │ │ │ ├── endpoints.test.ts │ │ │ ├── extension.test.ts │ │ │ ├── sanity.sanity-test.ts │ │ │ ├── services.ts │ │ │ ├── session.test.ts │ │ │ └── textDocumentManager.test.ts │ │ ├── testing/ │ │ │ ├── common/ │ │ │ │ └── files.ts │ │ │ ├── node/ │ │ │ │ ├── aiEvaluationService.tsx │ │ │ │ └── setupTestsFileManager.tsx │ │ │ └── vscode/ │ │ │ └── setupTestContributions.ts │ │ ├── tools/ │ │ │ ├── common/ │ │ │ │ ├── agentMemoryService.ts │ │ │ │ ├── askQuestionsTypes.ts │ │ │ │ ├── editToolLearningService.ts │ │ │ │ ├── editToolLearningStates.ts │ │ │ │ ├── memoryCleanupService.ts │ │ │ │ ├── test/ │ │ │ │ │ ├── agentMemoryService.spec.ts │ │ │ │ │ ├── toolNames.spec.ts │ │ │ │ │ └── toolService.spec.ts │ │ │ │ ├── toJsonSchema.ts │ │ │ │ ├── toolNames.ts │ │ │ │ ├── toolSchemaNormalizer.ts │ │ │ │ ├── toolUtils.ts │ │ │ │ ├── toolsRegistry.ts │ │ │ │ ├── toolsService.ts │ │ │ │ └── virtualTools/ │ │ │ │ ├── builtInToolGroupHandler.ts │ │ │ │ ├── preComputedToolEmbeddingsCache.ts │ │ │ │ ├── toolEmbeddingsComputer.ts │ │ │ │ ├── toolEmbeddingsLocalCache.ts │ │ │ │ ├── toolGrouping.ts │ │ │ │ ├── toolGroupingService.ts │ │ │ │ ├── virtualTool.ts │ │ │ │ ├── virtualToolGroupCache.ts │ │ │ │ ├── virtualToolGrouper.ts │ │ │ │ ├── virtualToolSummarizer.tsx │ │ │ │ ├── virtualToolTypes.ts │ │ │ │ └── virtualToolsConstants.ts │ │ │ ├── node/ │ │ │ │ ├── abstractReplaceStringTool.tsx │ │ │ │ ├── allTools.ts │ │ │ │ ├── applyPatch/ │ │ │ │ │ ├── parseApplyPatch.ts │ │ │ │ │ └── parser.ts │ │ │ │ ├── applyPatchTool.tsx │ │ │ │ ├── codebaseTool.tsx │ │ │ │ ├── createDirectoryTool.tsx │ │ │ │ ├── createFileTool.tsx │ │ │ │ ├── editFileHealing.tsx │ │ │ │ ├── editFileToolResult.tsx │ │ │ │ ├── editFileToolUtils.tsx │ │ │ │ ├── editNotebookTool.tsx │ │ │ │ ├── executionSubagentTool.ts │ │ │ │ ├── findFilesTool.tsx │ │ │ │ ├── findTestsFilesTool.tsx │ │ │ │ ├── findTextInFilesTool.tsx │ │ │ │ ├── getErrorsTool.tsx │ │ │ │ ├── getNotebookCellOutputTool.tsx │ │ │ │ ├── getSearchViewResultsTool.tsx │ │ │ │ ├── githubRepoTool.tsx │ │ │ │ ├── imageToolUtils.ts │ │ │ │ ├── insertEditTool.tsx │ │ │ │ ├── installExtensionTool.tsx │ │ │ │ ├── listDirTool.tsx │ │ │ │ ├── manageTodoListTool.tsx │ │ │ │ ├── memoryContextPrompt.tsx │ │ │ │ ├── memoryTool.tsx │ │ │ │ ├── multiReplaceStringTool.tsx │ │ │ │ ├── newNotebookTool.tsx │ │ │ │ ├── newWorkspace/ │ │ │ │ │ ├── newWorkspaceTool.tsx │ │ │ │ │ └── projectSetupInfoTool.tsx │ │ │ │ ├── notebookSummaryTool.tsx │ │ │ │ ├── readFileTool.tsx │ │ │ │ ├── readProjectStructureTool.ts │ │ │ │ ├── replaceStringTool.tsx │ │ │ │ ├── resolveMemoryFileUriTool.tsx │ │ │ │ ├── runNotebookCellTool.tsx │ │ │ │ ├── scmChangesTool.ts │ │ │ │ ├── searchSubagentTool.ts │ │ │ │ ├── searchWorkspaceSymbolsTool.tsx │ │ │ │ ├── test/ │ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ │ ├── findFiles.spec.tsx.snap │ │ │ │ │ │ ├── getErrorsResult.spec.tsx.snap │ │ │ │ │ │ ├── getErrorsTool.spec.tsx.snap │ │ │ │ │ │ └── toolCalling.spec.tsx.snap │ │ │ │ │ ├── applyPatch.spec.ts │ │ │ │ │ ├── editFileToolUtils.spec.ts │ │ │ │ │ ├── editFileToolUtilsFixtures/ │ │ │ │ │ │ ├── crlf-input.json │ │ │ │ │ │ ├── crlf-output.json │ │ │ │ │ │ ├── crlf-tool-call.json │ │ │ │ │ │ ├── math-original.txt │ │ │ │ │ │ ├── multi-sr-bug-actual.txt │ │ │ │ │ │ └── multi-sr-bug-original.txt │ │ │ │ │ ├── editNotebookTool.spec.tsx │ │ │ │ │ ├── editToolLearningService.spec.ts │ │ │ │ │ ├── executionSubagentTool.spec.ts │ │ │ │ │ ├── findFiles.spec.tsx │ │ │ │ │ ├── findTextInFilesResult.spec.tsx │ │ │ │ │ ├── findTextInFilesTool.spec.tsx │ │ │ │ │ ├── getErrorsResult.spec.tsx │ │ │ │ │ ├── getErrorsTool.spec.tsx │ │ │ │ │ ├── imageToolUtils.spec.ts │ │ │ │ │ ├── memoryTool.spec.tsx │ │ │ │ │ ├── multiReplaceStringTool.spec.tsx │ │ │ │ │ ├── readFile.spec.tsx │ │ │ │ │ ├── searchSubagentTool.spec.ts │ │ │ │ │ ├── searchToolTestUtils.ts │ │ │ │ │ ├── testFailure.spec.tsx │ │ │ │ │ ├── testTools.ts │ │ │ │ │ ├── testToolsService.ts │ │ │ │ │ ├── toJsonSchema.spec.ts │ │ │ │ │ ├── toolCalling.spec.tsx │ │ │ │ │ ├── toolTestUtils.tsx │ │ │ │ │ ├── toolUtils.spec.ts │ │ │ │ │ └── viewImage.spec.tsx │ │ │ │ ├── testFailureTool.tsx │ │ │ │ ├── todoListContextPrompt.tsx │ │ │ │ ├── toolReplayTool.tsx │ │ │ │ ├── toolSearchTool.ts │ │ │ │ ├── toolUtils.task.ts │ │ │ │ ├── toolUtils.ts │ │ │ │ ├── viewImageTool.tsx │ │ │ │ ├── vscodeAPITool.ts │ │ │ │ └── vscodeCmdTool.tsx │ │ │ ├── test/ │ │ │ │ ├── common/ │ │ │ │ │ └── toolSchemaNormalizer.spec.ts │ │ │ │ └── node/ │ │ │ │ ├── applyPatch/ │ │ │ │ │ ├── applyPatch.spec.tsx │ │ │ │ │ ├── corpus/ │ │ │ │ │ │ ├── 0.patch │ │ │ │ │ │ ├── 1.patch │ │ │ │ │ │ ├── 10.patch │ │ │ │ │ │ ├── 11.patch │ │ │ │ │ │ ├── 12.patch │ │ │ │ │ │ ├── 13.patch │ │ │ │ │ │ ├── 14.patch │ │ │ │ │ │ ├── 15.patch │ │ │ │ │ │ ├── 16.patch │ │ │ │ │ │ ├── 17.patch │ │ │ │ │ │ ├── 18.patch │ │ │ │ │ │ ├── 19.patch │ │ │ │ │ │ ├── 2.patch │ │ │ │ │ │ ├── 20.patch │ │ │ │ │ │ ├── 21.patch │ │ │ │ │ │ ├── 22.patch │ │ │ │ │ │ ├── 23.patch │ │ │ │ │ │ ├── 24.patch │ │ │ │ │ │ ├── 25.patch │ │ │ │ │ │ ├── 26.patch │ │ │ │ │ │ ├── 262549-call.txt │ │ │ │ │ │ ├── 262549-input.txt │ │ │ │ │ │ ├── 262549-output.txt │ │ │ │ │ │ ├── 267547-call.txt │ │ │ │ │ │ ├── 267547-input.txt │ │ │ │ │ │ ├── 267547-output.txt │ │ │ │ │ │ ├── 27.patch │ │ │ │ │ │ ├── 28.patch │ │ │ │ │ │ ├── 29.patch │ │ │ │ │ │ ├── 3.patch │ │ │ │ │ │ ├── 30.patch │ │ │ │ │ │ ├── 31.patch │ │ │ │ │ │ ├── 32.patch │ │ │ │ │ │ ├── 33.patch │ │ │ │ │ │ ├── 34.patch │ │ │ │ │ │ ├── 35.patch │ │ │ │ │ │ ├── 36.patch │ │ │ │ │ │ ├── 37.patch │ │ │ │ │ │ ├── 38.patch │ │ │ │ │ │ ├── 39.patch │ │ │ │ │ │ ├── 4.patch │ │ │ │ │ │ ├── 40.patch │ │ │ │ │ │ ├── 41.patch │ │ │ │ │ │ ├── 42.patch │ │ │ │ │ │ ├── 43.patch │ │ │ │ │ │ ├── 44.patch │ │ │ │ │ │ ├── 45.patch │ │ │ │ │ │ ├── 46.patch │ │ │ │ │ │ ├── 47.patch │ │ │ │ │ │ ├── 48.patch │ │ │ │ │ │ ├── 49.patch │ │ │ │ │ │ ├── 5.patch │ │ │ │ │ │ ├── 6.patch │ │ │ │ │ │ ├── 7.patch │ │ │ │ │ │ ├── 8.patch │ │ │ │ │ │ ├── 9.patch │ │ │ │ │ │ ├── multipleIndentedLines-call.txt │ │ │ │ │ │ ├── multipleIndentedLines-input.txt │ │ │ │ │ │ ├── multipleIndentedLines-output.txt │ │ │ │ │ │ ├── multipleSections-call.txt │ │ │ │ │ │ ├── multipleSections-input.txt │ │ │ │ │ │ ├── multipleSections-output.txt │ │ │ │ │ │ ├── reindent-call.txt │ │ │ │ │ │ └── reindent-input.txt │ │ │ │ │ ├── fixtures/ │ │ │ │ │ │ ├── 4302.ts.txt │ │ │ │ │ │ └── 4302.ts.txt.expected │ │ │ │ │ └── parser.spec.ts │ │ │ │ ├── replaceString/ │ │ │ │ │ ├── fixtures/ │ │ │ │ │ │ ├── math.js.txt │ │ │ │ │ │ ├── math.js.txt.expected │ │ │ │ │ │ └── settingsjson.txt │ │ │ │ │ └── replaceStringTool.spec.tsx │ │ │ │ └── virtualTools/ │ │ │ │ ├── testVirtualTools.ts │ │ │ │ ├── toolEmbeddingsCache.spec.ts │ │ │ │ ├── toolEmbeddingsLocalCache.spec.ts │ │ │ │ ├── virtualToolGrouper.spec.ts │ │ │ │ └── virtualToolGrouping.spec.ts │ │ │ └── vscode-node/ │ │ │ ├── allTools.ts │ │ │ ├── fetchWebPageTool.tsx │ │ │ ├── switchAgentTool.ts │ │ │ ├── test/ │ │ │ │ └── inputGlobToPattern.test.ts │ │ │ ├── tools.ts │ │ │ └── toolsService.ts │ │ ├── trajectory/ │ │ │ ├── ARCHITECTURE.md │ │ │ └── vscode-node/ │ │ │ ├── otelChatDebugLogProvider.ts │ │ │ ├── otelSpanToChatDebugEvent.ts │ │ │ ├── otlpFormatConversion.ts │ │ │ ├── test/ │ │ │ │ ├── otelSpanToChatDebugEvent.spec.ts │ │ │ │ └── otlpFormatConversion.spec.ts │ │ │ └── trajectoryExportCommands.ts │ │ ├── typescriptContext/ │ │ │ ├── DEVELOPMENT.md │ │ │ ├── common/ │ │ │ │ └── serverProtocol.ts │ │ │ ├── serverPlugin/ │ │ │ │ ├── .esbuild.ts │ │ │ │ ├── .gitignore │ │ │ │ ├── .npmignore │ │ │ │ ├── fixtures/ │ │ │ │ │ ├── context/ │ │ │ │ │ │ ├── p1/ │ │ │ │ │ │ │ ├── source/ │ │ │ │ │ │ │ │ ├── f1.ts │ │ │ │ │ │ │ │ ├── f2.ts │ │ │ │ │ │ │ │ ├── f3.ts │ │ │ │ │ │ │ │ └── f4.ts │ │ │ │ │ │ │ └── tsconfig.json │ │ │ │ │ │ ├── p10/ │ │ │ │ │ │ │ ├── source/ │ │ │ │ │ │ │ │ ├── f1.ts │ │ │ │ │ │ │ │ ├── f2.ts │ │ │ │ │ │ │ │ └── f3.ts │ │ │ │ │ │ │ └── tsconfig.json │ │ │ │ │ │ ├── p11/ │ │ │ │ │ │ │ ├── source/ │ │ │ │ │ │ │ │ ├── f1.ts │ │ │ │ │ │ │ │ ├── f2.ts │ │ │ │ │ │ │ │ └── f3.ts │ │ │ │ │ │ │ └── tsconfig.json │ │ │ │ │ │ ├── p12/ │ │ │ │ │ │ │ ├── source/ │ │ │ │ │ │ │ │ ├── f1.ts │ │ │ │ │ │ │ │ ├── f2.ts │ │ │ │ │ │ │ │ ├── f3.ts │ │ │ │ │ │ │ │ ├── f4.ts │ │ │ │ │ │ │ │ └── f5.ts │ │ │ │ │ │ │ └── tsconfig.json │ │ │ │ │ │ ├── p13/ │ │ │ │ │ │ │ ├── source/ │ │ │ │ │ │ │ │ ├── f1.ts │ │ │ │ │ │ │ │ ├── f2.ts │ │ │ │ │ │ │ │ └── f3.ts │ │ │ │ │ │ │ └── tsconfig.json │ │ │ │ │ │ ├── p14/ │ │ │ │ │ │ │ ├── source/ │ │ │ │ │ │ │ │ ├── f1.ts │ │ │ │ │ │ │ │ ├── f2.ts │ │ │ │ │ │ │ │ ├── f3.ts │ │ │ │ │ │ │ │ ├── f4.ts │ │ │ │ │ │ │ │ ├── f5.ts │ │ │ │ │ │ │ │ └── f6.ts │ │ │ │ │ │ │ └── tsconfig.json │ │ │ │ │ │ ├── p2/ │ │ │ │ │ │ │ ├── source/ │ │ │ │ │ │ │ │ ├── f1.ts │ │ │ │ │ │ │ │ └── f2.ts │ │ │ │ │ │ │ └── tsconfig.json │ │ │ │ │ │ ├── p3/ │ │ │ │ │ │ │ ├── source/ │ │ │ │ │ │ │ │ ├── f1.ts │ │ │ │ │ │ │ │ └── f2.ts │ │ │ │ │ │ │ └── tsconfig.json │ │ │ │ │ │ ├── p4/ │ │ │ │ │ │ │ ├── source/ │ │ │ │ │ │ │ │ ├── f1.ts │ │ │ │ │ │ │ │ ├── f2.ts │ │ │ │ │ │ │ │ ├── f3.ts │ │ │ │ │ │ │ │ ├── f4.ts │ │ │ │ │ │ │ │ └── f5.ts │ │ │ │ │ │ │ └── tsconfig.json │ │ │ │ │ │ ├── p5/ │ │ │ │ │ │ │ ├── source/ │ │ │ │ │ │ │ │ ├── f1.ts │ │ │ │ │ │ │ │ ├── f2.ts │ │ │ │ │ │ │ │ └── f3.ts │ │ │ │ │ │ │ └── tsconfig.json │ │ │ │ │ │ ├── p6/ │ │ │ │ │ │ │ ├── source/ │ │ │ │ │ │ │ │ ├── f1.ts │ │ │ │ │ │ │ │ └── f2.ts │ │ │ │ │ │ │ └── tsconfig.json │ │ │ │ │ │ ├── p7/ │ │ │ │ │ │ │ ├── source/ │ │ │ │ │ │ │ │ ├── f1.ts │ │ │ │ │ │ │ │ └── f2.ts │ │ │ │ │ │ │ └── tsconfig.json │ │ │ │ │ │ ├── p8/ │ │ │ │ │ │ │ ├── source/ │ │ │ │ │ │ │ │ ├── f1.ts │ │ │ │ │ │ │ │ ├── f2.ts │ │ │ │ │ │ │ │ └── f3.ts │ │ │ │ │ │ │ └── tsconfig.json │ │ │ │ │ │ ├── p9/ │ │ │ │ │ │ │ ├── source/ │ │ │ │ │ │ │ │ ├── f1.ts │ │ │ │ │ │ │ │ ├── f2.ts │ │ │ │ │ │ │ │ ├── f3.ts │ │ │ │ │ │ │ │ └── f4.ts │ │ │ │ │ │ │ └── tsconfig.json │ │ │ │ │ │ └── testbed/ │ │ │ │ │ │ ├── src/ │ │ │ │ │ │ │ ├── company.ts │ │ │ │ │ │ │ ├── disposable.ts │ │ │ │ │ │ │ ├── employee.ts │ │ │ │ │ │ │ ├── entity.ts │ │ │ │ │ │ │ ├── eventProvider.ts │ │ │ │ │ │ │ ├── events.ts │ │ │ │ │ │ │ ├── legalEntity.ts │ │ │ │ │ │ │ ├── main.ts │ │ │ │ │ │ │ └── person.ts │ │ │ │ │ │ └── tsconfig.json │ │ │ │ │ ├── nes/ │ │ │ │ │ │ ├── p1/ │ │ │ │ │ │ │ ├── source/ │ │ │ │ │ │ │ │ └── test.ts │ │ │ │ │ │ │ └── tsconfig.json │ │ │ │ │ │ └── p2/ │ │ │ │ │ │ ├── source/ │ │ │ │ │ │ │ └── test.ts │ │ │ │ │ │ └── tsconfig.json │ │ │ │ │ └── readme.md │ │ │ │ ├── package.json │ │ │ │ ├── src/ │ │ │ │ │ ├── common/ │ │ │ │ │ │ ├── api.ts │ │ │ │ │ │ ├── baseContextProviders.ts │ │ │ │ │ │ ├── classContextProvider.ts │ │ │ │ │ │ ├── code.ts │ │ │ │ │ │ ├── contextProvider.ts │ │ │ │ │ │ ├── functionContextProvider.ts │ │ │ │ │ │ ├── host.ts │ │ │ │ │ │ ├── methodContextProvider.ts │ │ │ │ │ │ ├── moduleContextProvider.ts │ │ │ │ │ │ ├── nesRenameValidator.ts │ │ │ │ │ │ ├── nullContextProvider.ts │ │ │ │ │ │ ├── protocol.ts │ │ │ │ │ │ ├── sourceFileContextProvider.ts │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ ├── typescript.ts │ │ │ │ │ │ ├── typescripts.ts │ │ │ │ │ │ └── utils.ts │ │ │ │ │ └── node/ │ │ │ │ │ ├── create.ts │ │ │ │ │ ├── host.ts │ │ │ │ │ ├── main.ts │ │ │ │ │ └── test/ │ │ │ │ │ ├── languageServerProxy.ts │ │ │ │ │ ├── languageServices.ts │ │ │ │ │ ├── nes.spec.ts │ │ │ │ │ ├── simple.spec.ts │ │ │ │ │ └── testing.ts │ │ │ │ └── tsconfig.json │ │ │ └── vscode-node/ │ │ │ ├── inspector.ts │ │ │ ├── languageContextService.ts │ │ │ ├── nesRenameService.ts │ │ │ ├── throttledDebounce.ts │ │ │ └── types.ts │ │ ├── vscode.proposed.activeComment.d.ts │ │ ├── vscode.proposed.agentSessionsWorkspace.d.ts │ │ ├── vscode.proposed.aiRelatedInformation.d.ts │ │ ├── vscode.proposed.aiSettingsSearch.d.ts │ │ ├── vscode.proposed.aiTextSearchProvider.d.ts │ │ ├── vscode.proposed.authLearnMore.d.ts │ │ ├── vscode.proposed.chatBinaryReferenceData.d.ts │ │ ├── vscode.proposed.chatDebug.d.ts │ │ ├── vscode.proposed.chatHooks.d.ts │ │ ├── vscode.proposed.chatParticipantAdditions.d.ts │ │ ├── vscode.proposed.chatParticipantPrivate.d.ts │ │ ├── vscode.proposed.chatPromptFiles.d.ts │ │ ├── vscode.proposed.chatProvider.d.ts │ │ ├── vscode.proposed.chatReadonlyPromptReference.d.ts │ │ ├── vscode.proposed.chatReferenceBinaryData.d.ts │ │ ├── vscode.proposed.chatReferenceDiagnostic.d.ts │ │ ├── vscode.proposed.chatSessionsProvider.d.ts │ │ ├── vscode.proposed.chatStatusItem.d.ts │ │ ├── vscode.proposed.codeActionAI.d.ts │ │ ├── vscode.proposed.commentReveal.d.ts │ │ ├── vscode.proposed.contribChatEditorInlineGutterMenu.d.ts │ │ ├── vscode.proposed.contribCommentThreadAdditionalMenu.d.ts │ │ ├── vscode.proposed.contribCommentsViewThreadMenus.d.ts │ │ ├── vscode.proposed.contribDebugCreateConfiguration.d.ts │ │ ├── vscode.proposed.contribEditorContentMenu.d.ts │ │ ├── vscode.proposed.contribLanguageModelToolSets.d.ts │ │ ├── vscode.proposed.contribSourceControlInputBoxMenu.d.ts │ │ ├── vscode.proposed.dataChannels.d.ts │ │ ├── vscode.proposed.defaultChatParticipant.d.ts │ │ ├── vscode.proposed.devDeviceId.d.ts │ │ ├── vscode.proposed.documentFiltersExclusive.d.ts │ │ ├── vscode.proposed.embeddings.d.ts │ │ ├── vscode.proposed.environmentPower.d.ts │ │ ├── vscode.proposed.extensionsAny.d.ts │ │ ├── vscode.proposed.findFiles2.d.ts │ │ ├── vscode.proposed.findTextInFiles.d.ts │ │ ├── vscode.proposed.findTextInFiles2.d.ts │ │ ├── vscode.proposed.inlineCompletionsAdditions.d.ts │ │ ├── vscode.proposed.interactive.d.ts │ │ ├── vscode.proposed.languageModelCapabilities.d.ts │ │ ├── vscode.proposed.languageModelSystem.d.ts │ │ ├── vscode.proposed.languageModelThinkingPart.d.ts │ │ ├── vscode.proposed.languageModelToolResultAudience.d.ts │ │ ├── vscode.proposed.languageModelToolSupportsModel.d.ts │ │ ├── vscode.proposed.mappedEditsProvider.d.ts │ │ ├── vscode.proposed.mcpServerDefinitions.d.ts │ │ ├── vscode.proposed.newSymbolNamesProvider.d.ts │ │ ├── vscode.proposed.readonlyMessage.d.ts │ │ ├── vscode.proposed.resolvers.d.ts │ │ ├── vscode.proposed.scmInputBoxValueProvider.d.ts │ │ ├── vscode.proposed.tabInputMultiDiff.d.ts │ │ ├── vscode.proposed.taskExecutionTerminal.d.ts │ │ ├── vscode.proposed.taskProblemMatcherStatus.d.ts │ │ ├── vscode.proposed.terminalDataWriteEvent.d.ts │ │ ├── vscode.proposed.terminalExecuteCommandEvent.d.ts │ │ ├── vscode.proposed.terminalQuickFixProvider.d.ts │ │ ├── vscode.proposed.terminalSelection.d.ts │ │ ├── vscode.proposed.terminalTitle.d.ts │ │ ├── vscode.proposed.testObserver.d.ts │ │ ├── vscode.proposed.textDocumentChangeReason.d.ts │ │ ├── vscode.proposed.textSearchProvider.d.ts │ │ ├── vscode.proposed.textSearchProvider2.d.ts │ │ ├── vscode.proposed.workspaceTrust.d.ts │ │ ├── workspaceChunkSearch/ │ │ │ ├── node/ │ │ │ │ └── workspaceChunkSearch.contribution.ts │ │ │ └── vscode-node/ │ │ │ ├── commands.ts │ │ │ ├── workspaceChunkSearch.contribution.ts │ │ │ └── workspaceIndexingStatus.ts │ │ ├── workspaceRecorder/ │ │ │ ├── common/ │ │ │ │ ├── jsonlUtil.ts │ │ │ │ └── workspaceListenerService.ts │ │ │ └── vscode-node/ │ │ │ ├── safeFileWriteUtils.ts │ │ │ ├── utils.ts │ │ │ ├── utilsObservable.ts │ │ │ ├── workspaceListenerService.ts │ │ │ ├── workspaceRecorder.ts │ │ │ └── workspaceRecorderFeature.ts │ │ ├── workspaceSemanticSearch/ │ │ │ └── node/ │ │ │ ├── combinedRank.ts │ │ │ ├── semanticSearchTextSearchProvider.ts │ │ │ └── test/ │ │ │ └── ranking.spec.ts │ │ └── xtab/ │ │ ├── common/ │ │ │ ├── diffHistoryForPrompt.ts │ │ │ ├── inlineSuggestion.ts │ │ │ ├── lintErrors.ts │ │ │ ├── promptCrafting.ts │ │ │ ├── promptCraftingUtils.ts │ │ │ ├── recentFilesForPrompt.md │ │ │ ├── recentFilesForPrompt.ts │ │ │ ├── similarFilesContextService.ts │ │ │ ├── systemMessages.ts │ │ │ ├── tags.ts │ │ │ ├── terminalOutput.ts │ │ │ └── xtabCurrentDocument.ts │ │ ├── node/ │ │ │ ├── xtabCustomDiffPatchResponseHandler.ts │ │ │ ├── xtabEndpoint.ts │ │ │ ├── xtabNextCursorPredictor.ts │ │ │ ├── xtabProvider.ts │ │ │ └── xtabUtils.ts │ │ └── test/ │ │ ├── common/ │ │ │ ├── inlineSuggestion.spec.ts │ │ │ ├── lintErrors.spec.ts │ │ │ ├── promptCrafting.spec.ts │ │ │ ├── recentFilesForPrompt.snapshots.spec.ts │ │ │ ├── recentFilesForPrompt.spec.ts │ │ │ └── responseProcessor.spec.ts │ │ └── node/ │ │ ├── diffHistoryForPrompt.spec.ts │ │ ├── editIntent.spec.ts │ │ ├── xtabCustomDiffPatchResponseHandler.spec.ts │ │ ├── xtabNextCursorPredictor.spec.ts │ │ └── xtabProvider.spec.ts │ ├── lib/ │ │ ├── node/ │ │ │ └── chatLibMain.ts │ │ └── vscode-node/ │ │ └── test/ │ │ ├── getInlineCompletions.reply.txt │ │ ├── getInlineCompletions.spec.ts │ │ ├── nesProvider.reply.txt │ │ ├── nesProvider.spec.ts │ │ └── simpleExperimentationService.spec.ts │ ├── platform/ │ │ ├── authentication/ │ │ │ ├── common/ │ │ │ │ ├── authentication.ts │ │ │ │ ├── authenticationUpgrade.ts │ │ │ │ ├── authenticationUpgradeService.ts │ │ │ │ ├── copilotToken.ts │ │ │ │ ├── copilotTokenManager.ts │ │ │ │ ├── copilotTokenStore.ts │ │ │ │ └── staticGitHubAuthenticationService.ts │ │ │ ├── node/ │ │ │ │ └── copilotTokenManager.ts │ │ │ ├── test/ │ │ │ │ └── node/ │ │ │ │ ├── authentication.spec.ts │ │ │ │ ├── copilotToken.spec.ts │ │ │ │ └── simulationTestCopilotTokenManager.ts │ │ │ └── vscode-node/ │ │ │ ├── authenticationService.ts │ │ │ ├── copilotTokenManager.ts │ │ │ └── session.ts │ │ ├── chat/ │ │ │ ├── common/ │ │ │ │ ├── blockedExtensionService.ts │ │ │ │ ├── chatAgents.ts │ │ │ │ ├── chatDebugFileLoggerService.ts │ │ │ │ ├── chatHookService.ts │ │ │ │ ├── chatMLFetcher.ts │ │ │ │ ├── chatQuotaService.ts │ │ │ │ ├── chatQuotaServiceImpl.ts │ │ │ │ ├── chatSessionService.ts │ │ │ │ ├── commonTypes.ts │ │ │ │ ├── conversationOptions.ts │ │ │ │ ├── globalStringUtils.ts │ │ │ │ ├── hookCommandTypes.ts │ │ │ │ ├── hookExecutor.ts │ │ │ │ ├── hooksOutputChannel.ts │ │ │ │ ├── interactionService.ts │ │ │ │ ├── responses.ts │ │ │ │ └── sessionTranscriptService.ts │ │ │ ├── node/ │ │ │ │ └── hookExecutor.ts │ │ │ ├── test/ │ │ │ │ ├── common/ │ │ │ │ │ ├── mockChatMLFetcher.ts │ │ │ │ │ ├── staticChatMLFetcher.ts │ │ │ │ │ ├── streamingMockChatMLFetcher.ts │ │ │ │ │ └── testChatSessionService.ts │ │ │ │ └── node/ │ │ │ │ └── hookExecutor.spec.ts │ │ │ └── vscode/ │ │ │ └── chatSessionService.ts │ │ ├── chunking/ │ │ │ ├── common/ │ │ │ │ ├── chunk.ts │ │ │ │ ├── chunkingEndpointClient.ts │ │ │ │ ├── chunkingEndpointClientImpl.ts │ │ │ │ └── chunkingStringUtils.ts │ │ │ └── node/ │ │ │ ├── naiveChunker.ts │ │ │ ├── naiveChunkerService.ts │ │ │ └── test/ │ │ │ └── naiveChunker.spec.ts │ │ ├── commands/ │ │ │ ├── common/ │ │ │ │ ├── mockRunCommandExecutionService.ts │ │ │ │ └── runCommandExecutionService.ts │ │ │ └── vscode/ │ │ │ └── runCommandExecutionServiceImpl.ts │ │ ├── completions-core/ │ │ │ └── common/ │ │ │ └── openai/ │ │ │ └── copilotAnnotations.ts │ │ ├── configuration/ │ │ │ ├── common/ │ │ │ │ ├── configurationService.ts │ │ │ │ ├── defaultsOnlyConfigurationService.ts │ │ │ │ ├── jsonSchema.ts │ │ │ │ ├── jsonSchemaDraft7.ts │ │ │ │ └── validator.ts │ │ │ ├── test/ │ │ │ │ └── common/ │ │ │ │ ├── inMemoryConfigurationService.ts │ │ │ │ └── validator.spec.ts │ │ │ └── vscode/ │ │ │ └── configurationServiceImpl.ts │ │ ├── customInstructions/ │ │ │ ├── common/ │ │ │ │ ├── customInstructionsService.ts │ │ │ │ └── promptTypes.ts │ │ │ └── test/ │ │ │ └── node/ │ │ │ └── customInstructionsService.spec.ts │ │ ├── debug/ │ │ │ ├── common/ │ │ │ │ └── debugOutputService.ts │ │ │ └── vscode/ │ │ │ ├── debugOutputListener.ts │ │ │ └── debugOutputServiceImpl.ts │ │ ├── devcontainer/ │ │ │ └── common/ │ │ │ └── devContainerConfigurationService.ts │ │ ├── dialog/ │ │ │ ├── common/ │ │ │ │ └── dialogService.ts │ │ │ └── vscode/ │ │ │ └── dialogServiceImpl.ts │ │ ├── diff/ │ │ │ ├── common/ │ │ │ │ ├── diffService.ts │ │ │ │ └── diffWorker.ts │ │ │ └── node/ │ │ │ ├── diffServiceImpl.ts │ │ │ └── diffWorkerMain.ts │ │ ├── editSurvivalTracking/ │ │ │ ├── common/ │ │ │ │ ├── arcTracker.ts │ │ │ │ ├── editCollector.ts │ │ │ │ ├── editComputer.ts │ │ │ │ ├── editSurvivalReporter.ts │ │ │ │ ├── editSurvivalTracker.ts │ │ │ │ └── editSurvivalTrackerService.ts │ │ │ └── test/ │ │ │ └── common/ │ │ │ └── editCollector.spec.ts │ │ ├── editing/ │ │ │ ├── common/ │ │ │ │ ├── abstractText.ts │ │ │ │ ├── edit.ts │ │ │ │ ├── edits.ts │ │ │ │ ├── notebookDocumentSnapshot.ts │ │ │ │ ├── offsetLineColumnConverter.ts │ │ │ │ ├── positionOffsetTransformer.ts │ │ │ │ └── textDocumentSnapshot.ts │ │ │ └── node/ │ │ │ └── edits.spec.ts │ │ ├── embeddings/ │ │ │ ├── common/ │ │ │ │ ├── embeddingsComputer.ts │ │ │ │ ├── embeddingsGrouper.ts │ │ │ │ ├── embeddingsIndex.ts │ │ │ │ ├── embeddingsStorage.ts │ │ │ │ ├── remoteEmbeddingsComputer.ts │ │ │ │ └── vscodeIndex.ts │ │ │ └── test/ │ │ │ └── node/ │ │ │ ├── embeddingsGrouper.spec.ts │ │ │ └── packEmbedding.spec.ts │ │ ├── endpoint/ │ │ │ ├── common/ │ │ │ │ ├── capiClient.ts │ │ │ │ ├── chatModelCapabilities.ts │ │ │ │ ├── compactionDataContainer.tsx │ │ │ │ ├── domainService.ts │ │ │ │ ├── endpointProvider.ts │ │ │ │ ├── endpointTypes.ts │ │ │ │ ├── licenseAgreement.ts │ │ │ │ ├── modelAliasRegistry.ts │ │ │ │ ├── phaseDataContainer.tsx │ │ │ │ ├── statefulMarkerContainer.tsx │ │ │ │ └── thinkingDataContainer.tsx │ │ │ ├── node/ │ │ │ │ ├── autoChatEndpoint.ts │ │ │ │ ├── automodeService.ts │ │ │ │ ├── capiClientImpl.ts │ │ │ │ ├── chatEndpoint.ts │ │ │ │ ├── copilotChatEndpoint.ts │ │ │ │ ├── domainServiceImpl.ts │ │ │ │ ├── embeddingsEndpoint.ts │ │ │ │ ├── messagesApi.ts │ │ │ │ ├── modelMetadataFetcher.ts │ │ │ │ ├── proxy4oEndpoint.ts │ │ │ │ ├── proxyAgenticSearchEndpoint.ts │ │ │ │ ├── proxyInstantApplyShortEndpoint.ts │ │ │ │ ├── proxyModelHelper.ts │ │ │ │ ├── proxyXtabEndpoint.ts │ │ │ │ ├── responsesApi.ts │ │ │ │ ├── routerDecisionFetcher.ts │ │ │ │ └── test/ │ │ │ │ ├── automodeService.spec.ts │ │ │ │ ├── copilotChatEndpoint.spec.ts │ │ │ │ └── responsesApi.spec.ts │ │ │ ├── test/ │ │ │ │ └── node/ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ └── stream.sseProcessor.spec.ts.snap │ │ │ │ ├── azureEndpoint.ts │ │ │ │ ├── capiEndpoint.ts │ │ │ │ ├── chatModelCapabilities.spec.ts │ │ │ │ ├── customNesEndpoint.ts │ │ │ │ ├── messagesApi.spec.ts │ │ │ │ ├── mockEndpoint.ts │ │ │ │ ├── openaiCompatibleEndpoint.ts │ │ │ │ ├── stream.splitChunk.spec.ts │ │ │ │ ├── stream.sseProcessor.spec.ts │ │ │ │ ├── test/ │ │ │ │ │ └── openaiCompatibleEndpoint.spec.ts │ │ │ │ └── testEndpointProvider.ts │ │ │ └── vscode-node/ │ │ │ ├── extChatEndpoint.ts │ │ │ ├── extChatTokenizer.ts │ │ │ └── test/ │ │ │ └── extChatTokenizer.spec.ts │ │ ├── env/ │ │ │ ├── common/ │ │ │ │ ├── envService.ts │ │ │ │ ├── nullEnvService.ts │ │ │ │ └── packagejson.ts │ │ │ ├── vscode/ │ │ │ │ └── envServiceImpl.ts │ │ │ └── vscode-node/ │ │ │ └── nativeEnvServiceImpl.ts │ │ ├── extContext/ │ │ │ └── common/ │ │ │ └── extensionContext.ts │ │ ├── extensions/ │ │ │ ├── common/ │ │ │ │ ├── extensionsService.ts │ │ │ │ └── packageJson.ts │ │ │ └── vscode/ │ │ │ └── extensionsService.ts │ │ ├── filesystem/ │ │ │ ├── common/ │ │ │ │ ├── fileSystemService.ts │ │ │ │ └── fileTypes.ts │ │ │ ├── node/ │ │ │ │ ├── fileSystemServiceImpl.ts │ │ │ │ └── test/ │ │ │ │ └── mockFileSystemService.ts │ │ │ └── vscode/ │ │ │ └── fileSystemServiceImpl.ts │ │ ├── git/ │ │ │ ├── common/ │ │ │ │ ├── gitCommitMessageService.ts │ │ │ │ ├── gitDiffService.ts │ │ │ │ ├── gitExtensionService.ts │ │ │ │ ├── gitService.ts │ │ │ │ ├── nullGitDiffService.ts │ │ │ │ ├── nullGitExtensionService.ts │ │ │ │ └── utils.ts │ │ │ ├── test/ │ │ │ │ └── node/ │ │ │ │ └── gitService.spec.ts │ │ │ └── vscode/ │ │ │ ├── git.d.ts │ │ │ ├── gitExtensionServiceImpl.ts │ │ │ └── gitServiceImpl.ts │ │ ├── github/ │ │ │ ├── common/ │ │ │ │ ├── githubAPI.ts │ │ │ │ ├── githubApiFetcherService.ts │ │ │ │ ├── githubService.ts │ │ │ │ ├── nullOctokitServiceImpl.ts │ │ │ │ └── octoKitServiceImpl.ts │ │ │ └── node/ │ │ │ └── githubRepositoryService.ts │ │ ├── ignore/ │ │ │ ├── common/ │ │ │ │ └── ignoreService.ts │ │ │ ├── node/ │ │ │ │ ├── ignoreFile.ts │ │ │ │ ├── ignoreServiceImpl.ts │ │ │ │ ├── remoteContentExclusion.ts │ │ │ │ └── test/ │ │ │ │ ├── mockAuthenticationService.ts │ │ │ │ ├── mockCAPIClientService.ts │ │ │ │ ├── mockGitService.ts │ │ │ │ ├── mockWorkspaceService.ts │ │ │ │ └── remoteContentExclusion.spec.ts │ │ │ ├── vscode/ │ │ │ │ └── ignoreInfoFileContentProvider.ts │ │ │ └── vscode-node/ │ │ │ └── ignoreService.ts │ │ ├── image/ │ │ │ ├── common/ │ │ │ │ └── imageService.ts │ │ │ ├── node/ │ │ │ │ └── imageServiceImpl.ts │ │ │ └── vscode-node/ │ │ │ └── imageServiceImpl.ts │ │ ├── inlineCompletions/ │ │ │ └── common/ │ │ │ └── api.ts │ │ ├── inlineEdits/ │ │ │ ├── common/ │ │ │ │ ├── dataTypes/ │ │ │ │ │ ├── codeActionData.ts │ │ │ │ │ ├── diagnosticData.ts │ │ │ │ │ ├── documentId.ts │ │ │ │ │ ├── edit.ts │ │ │ │ │ ├── editUtils.ts │ │ │ │ │ ├── fetchCancellationError.ts │ │ │ │ │ ├── importFilteringOptions.ts │ │ │ │ │ ├── inlineEditsModelsTypes.ts │ │ │ │ │ ├── jointCompletionsProviderOptions.ts │ │ │ │ │ ├── languageContext.ts │ │ │ │ │ ├── languageId.ts │ │ │ │ │ ├── nextCursorLinePrediction.ts │ │ │ │ │ ├── permutation.ts │ │ │ │ │ ├── rootedLineEdit.ts │ │ │ │ │ ├── textEditLength.ts │ │ │ │ │ ├── textEditLengthHelper/ │ │ │ │ │ │ ├── README.md │ │ │ │ │ │ ├── combineTextEditInfos.ts │ │ │ │ │ │ ├── length.ts │ │ │ │ │ │ └── textEditInfo.ts │ │ │ │ │ ├── triggerOptions.ts │ │ │ │ │ ├── xtabHistoryOptions.ts │ │ │ │ │ └── xtabPromptOptions.ts │ │ │ │ ├── debugRecorderBookmark.ts │ │ │ │ ├── editReason.ts │ │ │ │ ├── inlineEditLogContext.ts │ │ │ │ ├── inlineEditsModelService.ts │ │ │ │ ├── nesActivationStatusTelemetry.contribution.ts │ │ │ │ ├── notebook.ts │ │ │ │ ├── observableGit.ts │ │ │ │ ├── observableWorkspace.ts │ │ │ │ ├── responseProcessor.ts │ │ │ │ ├── statelessNextEditProvider.ts │ │ │ │ ├── statelessNextEditProviders.ts │ │ │ │ ├── utils/ │ │ │ │ │ ├── observable.ts │ │ │ │ │ ├── stringifyChatMessages.ts │ │ │ │ │ ├── tsExpr.ts │ │ │ │ │ └── utils.ts │ │ │ │ └── workspaceEditTracker/ │ │ │ │ ├── historyContextProvider.ts │ │ │ │ ├── nesHistoryContextProvider.ts │ │ │ │ ├── nesXtabHistoryTracker.ts │ │ │ │ ├── singleFileStaticWorkspaceEditTracker.ts │ │ │ │ ├── staticWorkspaceEditTracker.ts │ │ │ │ └── workspaceDocumentEditTracker.ts │ │ │ ├── node/ │ │ │ │ └── inlineEditsModelService.ts │ │ │ └── test/ │ │ │ ├── common/ │ │ │ │ ├── statelessNextEditProviers.spec.ts │ │ │ │ └── textEditLength.spec.ts │ │ │ └── node/ │ │ │ ├── edits.spec.ts │ │ │ └── random.ts │ │ ├── interactive/ │ │ │ ├── common/ │ │ │ │ └── interactiveSessionService.ts │ │ │ └── vscode/ │ │ │ └── interactiveSessionServiceImpl.ts │ │ ├── languageContextProvider/ │ │ │ └── common/ │ │ │ ├── languageContextProviderService.ts │ │ │ └── nullLanguageContextProviderService.ts │ │ ├── languageServer/ │ │ │ └── common/ │ │ │ └── languageContextService.ts │ │ ├── languages/ │ │ │ ├── common/ │ │ │ │ ├── languageDiagnosticsService.ts │ │ │ │ ├── languageFeaturesService.ts │ │ │ │ └── testLanguageDiagnosticsService.ts │ │ │ └── vscode/ │ │ │ ├── languageDiagnosticsServiceImpl.ts │ │ │ └── languageFeaturesServicesImpl.ts │ │ ├── log/ │ │ │ ├── common/ │ │ │ │ ├── logExecTime.ts │ │ │ │ ├── logService.ts │ │ │ │ └── messageStringify.ts │ │ │ ├── test/ │ │ │ │ └── common/ │ │ │ │ ├── loggerHelpers.ts │ │ │ │ └── subLogger.spec.ts │ │ │ └── vscode/ │ │ │ └── outputChannelLogTarget.ts │ │ ├── mcp/ │ │ │ ├── common/ │ │ │ │ └── mcpService.ts │ │ │ └── vscode/ │ │ │ ├── mcpServiceImpl.ts │ │ │ └── test/ │ │ │ └── mcpService.spec.ts │ │ ├── multiFileEdit/ │ │ │ └── common/ │ │ │ ├── editLogService.ts │ │ │ └── multiFileEditQualityTelemetry.ts │ │ ├── nesFetch/ │ │ │ ├── common/ │ │ │ │ ├── completionHelpers.ts │ │ │ │ ├── completionsAPI.ts │ │ │ │ ├── completionsFetchService.ts │ │ │ │ └── responseStream.ts │ │ │ └── node/ │ │ │ ├── completionsFetchServiceImpl.ts │ │ │ └── streamTransformer.ts │ │ ├── networking/ │ │ │ ├── common/ │ │ │ │ ├── anthropic.ts │ │ │ │ ├── fetch.ts │ │ │ │ ├── fetcherService.ts │ │ │ │ ├── networking.ts │ │ │ │ ├── openai.ts │ │ │ │ └── responseConvert.ts │ │ │ ├── node/ │ │ │ │ ├── baseFetchFetcher.ts │ │ │ │ ├── chatStream.ts │ │ │ │ ├── chatWebSocketManager.ts │ │ │ │ ├── chatWebSocketTelemetry.ts │ │ │ │ ├── fetcherFallback.ts │ │ │ │ ├── nodeFetchFetcher.ts │ │ │ │ ├── nodeFetcher.ts │ │ │ │ ├── stream.ts │ │ │ │ └── test/ │ │ │ │ ├── chatWebSocketManager.spec.ts │ │ │ │ ├── createWebSocket.spec.ts │ │ │ │ └── nodeFetcherService.ts │ │ │ ├── test/ │ │ │ │ └── node/ │ │ │ │ ├── fetcherFallback.spec.ts │ │ │ │ ├── headerContributors.spec.ts │ │ │ │ └── networking.spec.ts │ │ │ └── vscode-node/ │ │ │ ├── electronFetcher.ts │ │ │ ├── fetcherServiceImpl.ts │ │ │ └── test/ │ │ │ └── fetcherServiceCrash.spec.ts │ │ ├── notebook/ │ │ │ ├── common/ │ │ │ │ ├── alternativeContent.ts │ │ │ │ ├── alternativeContentEditGenerator.ts │ │ │ │ ├── alternativeContentFormat.ts │ │ │ │ ├── alternativeContentProvider.json.ts │ │ │ │ ├── alternativeContentProvider.text.ts │ │ │ │ ├── alternativeContentProvider.ts │ │ │ │ ├── alternativeContentProvider.xml.ts │ │ │ │ ├── alternativeNotebookDocument.ts │ │ │ │ ├── alternativeNotebookTextDocument.ts │ │ │ │ ├── helpers.ts │ │ │ │ ├── mockAlternativeContentService.ts │ │ │ │ ├── notebookDiff.ts │ │ │ │ ├── notebookService.ts │ │ │ │ ├── notebookSummaryTracker.ts │ │ │ │ └── offsetTranslator.ts │ │ │ ├── test/ │ │ │ │ ├── common/ │ │ │ │ │ └── offsetTranslator.spec.tsx │ │ │ │ └── node/ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ └── alternativeNotebookTextDocument.spec.tsx.snap │ │ │ │ ├── alternativeContent.spec.ts │ │ │ │ ├── alternativeContentEditGenerator.spec.ts │ │ │ │ ├── alternativeNotebookTextDocument.spec.tsx │ │ │ │ ├── fixtures/ │ │ │ │ │ ├── circle_area_edits.altContent.json │ │ │ │ │ ├── circle_area_edits.altContent.text │ │ │ │ │ ├── circle_area_edits.altContent.xml │ │ │ │ │ ├── circle_area_edits_after.ipynb │ │ │ │ │ ├── circle_area_edits_before.ipynb │ │ │ │ │ ├── data_processing.altContent.json │ │ │ │ │ ├── data_processing.altContent.text │ │ │ │ │ ├── data_processing.altContent.xml │ │ │ │ │ ├── data_processing_2.altContent.json │ │ │ │ │ ├── data_processing_2.altContent.text │ │ │ │ │ ├── data_processing_2.altContent.xml │ │ │ │ │ ├── data_processing_2_after.ipynb │ │ │ │ │ ├── data_processing_2_before.ipynb │ │ │ │ │ ├── data_processing_after.ipynb │ │ │ │ │ ├── data_processing_before.ipynb │ │ │ │ │ ├── data_visualization.altContent.json │ │ │ │ │ ├── data_visualization.altContent.text │ │ │ │ │ ├── data_visualization.altContent.xml │ │ │ │ │ ├── data_visualization_2.altContent.json │ │ │ │ │ ├── data_visualization_2.altContent.text │ │ │ │ │ ├── data_visualization_2.altContent.xml │ │ │ │ │ ├── data_visualization_2_after.ipynb │ │ │ │ │ ├── data_visualization_2_before.ipynb │ │ │ │ │ ├── data_visualization_after.ipynb │ │ │ │ │ ├── data_visualization_before.ipynb │ │ │ │ │ ├── datacleansing.altContent.json │ │ │ │ │ ├── datacleansing.altContent.text │ │ │ │ │ ├── datacleansing.altContent.xml │ │ │ │ │ ├── datacleansing_after.ipynb │ │ │ │ │ ├── datacleansing_before.ipynb │ │ │ │ │ ├── dataframe.altContent.json │ │ │ │ │ ├── dataframe.altContent.text │ │ │ │ │ ├── dataframe.altContent.xml │ │ │ │ │ ├── dataframe_after.ipynb │ │ │ │ │ ├── dataframe_before.ipynb │ │ │ │ │ ├── delete_1_line_in_cell.altContent.text │ │ │ │ │ ├── delete_1_line_in_cell.altContent.xml │ │ │ │ │ ├── delete_1_line_in_cell_after.ipynb │ │ │ │ │ ├── delete_1_line_in_cell_before.ipynb │ │ │ │ │ ├── duplicateCellIds.ipynb │ │ │ │ │ ├── duplicateCellIds.xml │ │ │ │ │ ├── edit.altContent.json │ │ │ │ │ ├── edit.altContent.text │ │ │ │ │ ├── edit.altContent.xml │ │ │ │ │ ├── edit_after.ipynb │ │ │ │ │ ├── edit_before.ipynb │ │ │ │ │ ├── empty.altContent.json │ │ │ │ │ ├── empty.altContent.text │ │ │ │ │ ├── empty.altContent.xml │ │ │ │ │ ├── empty_after.ipynb │ │ │ │ │ ├── empty_before.ipynb │ │ │ │ │ ├── imports.altContent.json │ │ │ │ │ ├── imports.altContent.text │ │ │ │ │ ├── imports.altContent.xml │ │ │ │ │ ├── imports_after.ipynb │ │ │ │ │ ├── imports_before.ipynb │ │ │ │ │ ├── insert.1.ipynb.xml │ │ │ │ │ ├── insert.2.ipynb.xml │ │ │ │ │ ├── insert.3.ipynb.xml │ │ │ │ │ ├── insert.4.ipynb.xml │ │ │ │ │ ├── insert.ipynb │ │ │ │ │ ├── large_cell.altContent.json │ │ │ │ │ ├── large_cell.altContent.text │ │ │ │ │ ├── large_cell.altContent.xml │ │ │ │ │ ├── large_cell_after.ipynb │ │ │ │ │ ├── large_cell_before.ipynb │ │ │ │ │ ├── matplotlib_to_plotly_after.ipynb │ │ │ │ │ ├── matplotlib_to_plotly_before.ipynb │ │ │ │ │ ├── multicells.altContent.json │ │ │ │ │ ├── multicells.altContent.text │ │ │ │ │ ├── multicells.altContent.xml │ │ │ │ │ ├── multicells_after.ipynb │ │ │ │ │ ├── multicells_before.ipynb │ │ │ │ │ ├── plot.altContent.json │ │ │ │ │ ├── plot.altContent.text │ │ │ │ │ ├── plot.altContent.xml │ │ │ │ │ ├── plot_after.ipynb │ │ │ │ │ ├── plot_before.ipynb │ │ │ │ │ ├── plotly_to_matplotlib.altContent.text │ │ │ │ │ ├── plotly_to_matplotlib.altContent.xml │ │ │ │ │ ├── plotly_to_matplotlib_after.ipynb │ │ │ │ │ ├── plotly_to_matplotlib_before.ipynb │ │ │ │ │ ├── refactor.altContent.json │ │ │ │ │ ├── refactor.altContent.text │ │ │ │ │ ├── refactor.altContent.xml │ │ │ │ │ ├── refactor_after.ipynb │ │ │ │ │ ├── refactor_before.ipynb │ │ │ │ │ ├── reorder.altContent.json │ │ │ │ │ ├── reorder.altContent.text │ │ │ │ │ ├── reorder.altContent.xml │ │ │ │ │ ├── reorder_after.ipynb │ │ │ │ │ ├── reorder_before.ipynb │ │ │ │ │ ├── sample.github-issues │ │ │ │ │ ├── sample.github-issues.json │ │ │ │ │ ├── sample.github-issues.text │ │ │ │ │ ├── sample.github-issues.xml │ │ │ │ │ ├── sample.ipynb │ │ │ │ │ ├── sample.ipynb.json │ │ │ │ │ ├── sample.ipynb.text │ │ │ │ │ ├── sample.ipynb.xml │ │ │ │ │ ├── single.altContent.json │ │ │ │ │ ├── single.altContent.text │ │ │ │ │ ├── single.altContent.xml │ │ │ │ │ ├── single_after.ipynb │ │ │ │ │ ├── single_before.ipynb │ │ │ │ │ ├── swapping_cells.ipynb │ │ │ │ │ ├── variables.altContent.json │ │ │ │ │ ├── variables.altContent.text │ │ │ │ │ ├── variables.altContent.xml │ │ │ │ │ ├── variables_after.ipynb │ │ │ │ │ ├── variables_before.ipynb │ │ │ │ │ ├── withOutput.ipynb │ │ │ │ │ ├── withOutput.ipynb.json │ │ │ │ │ ├── withOutput.ipynb.text │ │ │ │ │ └── withOutput.ipynb.xml │ │ │ │ ├── notebookService.spec.ts │ │ │ │ └── utils.ts │ │ │ └── vscode/ │ │ │ ├── notebookExectionServiceImpl.ts │ │ │ ├── notebookServiceImpl.ts │ │ │ └── notebookSummaryTrackerImpl.ts │ │ ├── notification/ │ │ │ ├── common/ │ │ │ │ └── notificationService.ts │ │ │ └── vscode/ │ │ │ └── notificationServiceImpl.ts │ │ ├── open/ │ │ │ ├── common/ │ │ │ │ └── opener.ts │ │ │ └── vscode/ │ │ │ └── opener.ts │ │ ├── openai/ │ │ │ └── node/ │ │ │ ├── fetch.ts │ │ │ └── test/ │ │ │ └── chatTokens.spec.ts │ │ ├── otel/ │ │ │ ├── common/ │ │ │ │ ├── agentOTelEnv.ts │ │ │ │ ├── genAiAttributes.ts │ │ │ │ ├── genAiEvents.ts │ │ │ │ ├── genAiMetrics.ts │ │ │ │ ├── index.ts │ │ │ │ ├── messageFormatters.ts │ │ │ │ ├── noopOtelService.ts │ │ │ │ ├── otelConfig.ts │ │ │ │ ├── otelService.ts │ │ │ │ └── test/ │ │ │ │ ├── agentOTelEnv.spec.ts │ │ │ │ ├── agentTraceHierarchy.spec.ts │ │ │ │ ├── byokProviderSpans.spec.ts │ │ │ │ ├── capturingOTelService.ts │ │ │ │ ├── chatMLFetcherSpanLifecycle.spec.ts │ │ │ │ ├── genAiEvents.spec.ts │ │ │ │ ├── genAiMetrics.spec.ts │ │ │ │ ├── messageFormatters.spec.ts │ │ │ │ ├── noopOtelService.spec.ts │ │ │ │ ├── otelConfig.spec.ts │ │ │ │ └── serviceRobustness.spec.ts │ │ │ └── node/ │ │ │ ├── fileExporters.ts │ │ │ ├── inMemoryOTelService.ts │ │ │ ├── otelServiceImpl.ts │ │ │ └── test/ │ │ │ ├── fileExporters.spec.ts │ │ │ └── traceContextPropagation.spec.ts │ │ ├── parser/ │ │ │ ├── node/ │ │ │ │ ├── chunkGroupTypes.ts │ │ │ │ ├── docGenParsing.ts │ │ │ │ ├── indentationStructure.ts │ │ │ │ ├── languageLoader.ts │ │ │ │ ├── nodes.ts │ │ │ │ ├── parserImpl.ts │ │ │ │ ├── parserService.ts │ │ │ │ ├── parserServiceImpl.ts │ │ │ │ ├── parserWithCaching.ts │ │ │ │ ├── parserWorker.ts │ │ │ │ ├── querying.ts │ │ │ │ ├── selectionParsing.ts │ │ │ │ ├── structure.ts │ │ │ │ ├── testGenParsing.ts │ │ │ │ ├── treeSitterLanguages.ts │ │ │ │ ├── treeSitterQueries.ts │ │ │ │ └── util.ts │ │ │ └── test/ │ │ │ └── node/ │ │ │ ├── __snapshots__/ │ │ │ │ ├── getStructure.csharp.spec.ts.snap │ │ │ │ ├── getStructure.golang.spec.ts.snap │ │ │ │ ├── getStructure.js.spec.ts.snap │ │ │ │ ├── getStructure.py.spec.ts.snap │ │ │ │ ├── getStructure.ruby.spec.ts.snap │ │ │ │ ├── getStructure.ts.spec.ts.snap │ │ │ │ └── getStructure.tsx.spec.ts.snap │ │ │ ├── findLastTest/ │ │ │ │ ├── ts.spec.ts │ │ │ │ └── util.ts │ │ │ ├── fixtures/ │ │ │ │ ├── EditForm.tsx │ │ │ │ ├── chatSetup.ts │ │ │ │ ├── chatSetup.ts.getStructure.html │ │ │ │ ├── dllmain.cpp │ │ │ │ ├── dllmain.cpp.getStructure.html │ │ │ │ ├── problem1.cpp │ │ │ │ ├── problem1.cpp.getStructure.html │ │ │ │ ├── test.cpp │ │ │ │ ├── test.cpp.getStructure.html │ │ │ │ ├── test.cs │ │ │ │ ├── test.go │ │ │ │ ├── test.java │ │ │ │ ├── test.java.getStructure.html │ │ │ │ ├── test.js │ │ │ │ ├── test.py │ │ │ │ ├── test.rb │ │ │ │ ├── test.rs │ │ │ │ ├── test.rs.getStructure.html │ │ │ │ ├── test.tsx │ │ │ │ ├── try.py │ │ │ │ ├── try.py.getStructure.html │ │ │ │ ├── vscode.proposed.chatParticipantAdditions-annotated.d.ts.txt │ │ │ │ └── vscode.proposed.chatParticipantAdditions.d.ts │ │ │ ├── getNodeMatchingSelection.spec.ts │ │ │ ├── getNodeToDocument.cpp.spec.ts │ │ │ ├── getNodeToDocument.java.spec.ts │ │ │ ├── getNodeToDocument.ts.spec.ts │ │ │ ├── getNodeToDocument.util.ts │ │ │ ├── getParseErrorCount.spec.ts │ │ │ ├── getStructure.cpp.spec.ts │ │ │ ├── getStructure.csharp.spec.ts │ │ │ ├── getStructure.golang.spec.ts │ │ │ ├── getStructure.java.spec.ts │ │ │ ├── getStructure.js.spec.ts │ │ │ ├── getStructure.py.spec.ts │ │ │ ├── getStructure.ruby.spec.ts │ │ │ ├── getStructure.rust.spec.ts │ │ │ ├── getStructure.ts.spec.ts │ │ │ ├── getStructure.tsx.spec.ts │ │ │ ├── getStructure.util.ts │ │ │ ├── getTestableNode.js.spec.ts │ │ │ ├── getTestableNode.ts.spec.ts │ │ │ ├── getTestableNode.util.ts │ │ │ ├── getTestableNodes.ts.spec.ts │ │ │ ├── getTestableNodes.util.ts │ │ │ ├── indentationStructure.spec.ts │ │ │ ├── markers.ts │ │ │ └── parser.spec.ts │ │ ├── projectTemplatesIndex/ │ │ │ └── common/ │ │ │ └── projectTemplatesIndex.ts │ │ ├── promptFiles/ │ │ │ └── common/ │ │ │ ├── promptsService.ts │ │ │ └── promptsServiceImpl.ts │ │ ├── prompts/ │ │ │ ├── common/ │ │ │ │ └── promptPathRepresentationService.ts │ │ │ └── test/ │ │ │ └── node/ │ │ │ └── promptPathRepresentationService.spec.ts │ │ ├── proxyModels/ │ │ │ ├── common/ │ │ │ │ └── proxyModelsService.ts │ │ │ └── node/ │ │ │ └── proxyModelsService.ts │ │ ├── releaseNotes/ │ │ │ ├── common/ │ │ │ │ └── releaseNotesService.ts │ │ │ └── vscode/ │ │ │ └── releaseNotesServiceImpl.ts │ │ ├── remoteCodeSearch/ │ │ │ ├── common/ │ │ │ │ ├── adoCodeSearchService.ts │ │ │ │ ├── githubCodeSearchService.ts │ │ │ │ └── remoteCodeSearch.ts │ │ │ ├── node/ │ │ │ │ └── codeSearchRepoAuth.ts │ │ │ └── vscode-node/ │ │ │ └── codeSearchRepoAuth.ts │ │ ├── remoteRepositories/ │ │ │ ├── common/ │ │ │ │ └── utils.ts │ │ │ └── vscode/ │ │ │ └── remoteRepositories.ts │ │ ├── remoteSearch/ │ │ │ ├── common/ │ │ │ │ ├── codeOrDocsSearchClient.ts │ │ │ │ ├── codeOrDocsSearchErrors.ts │ │ │ │ └── utils.ts │ │ │ ├── node/ │ │ │ │ └── codeOrDocsSearchClientImpl.ts │ │ │ └── test/ │ │ │ └── node/ │ │ │ ├── codeOrDocsSearchErrors.spec.ts │ │ │ └── utils.spec.ts │ │ ├── requestLogger/ │ │ │ ├── common/ │ │ │ │ └── capturingToken.ts │ │ │ ├── node/ │ │ │ │ ├── nullRequestLogger.ts │ │ │ │ └── requestLogger.ts │ │ │ └── test/ │ │ │ └── node/ │ │ │ └── testRequestLogger.ts │ │ ├── review/ │ │ │ ├── common/ │ │ │ │ ├── reviewCommand.ts │ │ │ │ └── reviewService.ts │ │ │ └── vscode/ │ │ │ └── reviewServiceImpl.ts │ │ ├── scopeSelection/ │ │ │ ├── common/ │ │ │ │ └── scopeSelection.ts │ │ │ └── vscode-node/ │ │ │ └── scopeSelectionImpl.ts │ │ ├── search/ │ │ │ ├── common/ │ │ │ │ └── searchService.ts │ │ │ ├── vscode/ │ │ │ │ └── baseSearchServiceImpl.ts │ │ │ └── vscode-node/ │ │ │ └── searchServiceImpl.ts │ │ ├── settingsEditor/ │ │ │ └── common/ │ │ │ └── settingsEditorSearchService.ts │ │ ├── simulationTestContext/ │ │ │ └── common/ │ │ │ └── simulationTestContext.ts │ │ ├── snippy/ │ │ │ └── common/ │ │ │ ├── snippyCompute.ts │ │ │ ├── snippyFetcher.ts │ │ │ ├── snippyNotifier.ts │ │ │ ├── snippyService.ts │ │ │ ├── snippyServiceImpl.ts │ │ │ └── snippyTypes.ts │ │ ├── survey/ │ │ │ ├── common/ │ │ │ │ └── surveyService.ts │ │ │ └── vscode/ │ │ │ └── surveyServiceImpl.ts │ │ ├── tabs/ │ │ │ ├── common/ │ │ │ │ └── tabsAndEditorsService.ts │ │ │ └── vscode/ │ │ │ └── tabsAndEditorsServiceImpl.ts │ │ ├── tasks/ │ │ │ ├── common/ │ │ │ │ ├── tasksService.ts │ │ │ │ └── testTasksService.ts │ │ │ └── vscode/ │ │ │ └── tasksService.ts │ │ ├── telemetry/ │ │ │ ├── common/ │ │ │ │ ├── baseTelemetryService.ts │ │ │ │ ├── failingTelemetryReporter.ts │ │ │ │ ├── ghTelemetrySender.ts │ │ │ │ ├── ghTelemetryService.ts │ │ │ │ ├── msftTelemetrySender.ts │ │ │ │ ├── nullExperimentationService.ts │ │ │ │ ├── nullTelemetryService.ts │ │ │ │ ├── telemetry.ts │ │ │ │ └── telemetryData.ts │ │ │ ├── node/ │ │ │ │ ├── azureInsights.ts │ │ │ │ ├── azureInsightsReporter.ts │ │ │ │ ├── baseExperimentationService.ts │ │ │ │ └── spyingTelemetryService.ts │ │ │ ├── test/ │ │ │ │ └── node/ │ │ │ │ ├── experimentation.spec.ts │ │ │ │ ├── telemetry.spec.ts │ │ │ │ └── telemetry2.spec.ts │ │ │ └── vscode-node/ │ │ │ ├── githubTelemetrySender.ts │ │ │ ├── microsoftExperimentationService.ts │ │ │ ├── microsoftTelemetrySender.ts │ │ │ └── telemetryServiceImpl.ts │ │ ├── terminal/ │ │ │ ├── common/ │ │ │ │ └── terminalService.ts │ │ │ └── vscode/ │ │ │ ├── terminalBufferListener.ts │ │ │ └── terminalServiceImpl.ts │ │ ├── test/ │ │ │ ├── common/ │ │ │ │ ├── endpointTestFixtures.ts │ │ │ │ ├── testCustomInstructionsService.ts │ │ │ │ ├── testExtensionsService.ts │ │ │ │ └── testNotebookService.ts │ │ │ └── node/ │ │ │ ├── extensionContext.ts │ │ │ ├── fetcher.ts │ │ │ ├── isInExtensionHost.ts │ │ │ ├── promptContextModel.ts │ │ │ ├── services.ts │ │ │ ├── simulationWorkspace.ts │ │ │ ├── simulationWorkspaceServices.ts │ │ │ ├── telemetry.ts │ │ │ ├── telemetryFake.ts │ │ │ ├── testChatAgentService.ts │ │ │ ├── testHeaderContributor.ts │ │ │ ├── testWorkbenchService.ts │ │ │ └── testWorkspaceService.ts │ │ ├── testing/ │ │ │ ├── common/ │ │ │ │ ├── nullTestProvider.ts │ │ │ │ ├── nullWorkspaceMutationManager.ts │ │ │ │ ├── setupTestExtensions.ts │ │ │ │ ├── testLogService.ts │ │ │ │ ├── testProvider.ts │ │ │ │ └── workspaceMutationManager.ts │ │ │ ├── node/ │ │ │ │ ├── setupTestDetector.mmd │ │ │ │ ├── setupTestDetector.tsx │ │ │ │ └── testDepsResolver.ts │ │ │ ├── test/ │ │ │ │ └── node/ │ │ │ │ └── setupTestDetector.spec.ts │ │ │ └── vscode/ │ │ │ └── testProviderImpl.ts │ │ ├── tfidf/ │ │ │ └── node/ │ │ │ ├── test/ │ │ │ │ └── tfidf.spec.ts │ │ │ ├── tfidf.ts │ │ │ ├── tfidfMessaging.ts │ │ │ └── tfidfWorker.ts │ │ ├── thinking/ │ │ │ └── common/ │ │ │ ├── thinking.ts │ │ │ └── thinkingUtils.ts │ │ ├── tokenizer/ │ │ │ ├── node/ │ │ │ │ ├── cl100k_base.tiktoken │ │ │ │ ├── o200k_base.tiktoken │ │ │ │ ├── parseTikTokens.ts │ │ │ │ ├── promptTokenDetails.ts │ │ │ │ ├── tikTokenizerImpl.ts │ │ │ │ ├── tikTokenizerWorker.ts │ │ │ │ └── tokenizer.ts │ │ │ └── test/ │ │ │ └── node/ │ │ │ └── tokenizer.spec.ts │ │ ├── trajectory/ │ │ │ ├── common/ │ │ │ │ ├── trajectoryLogger.ts │ │ │ │ └── trajectoryTypes.ts │ │ │ └── node/ │ │ │ ├── trajectoryLogger.ts │ │ │ └── trajectoryLoggerAdapter.ts │ │ ├── urlChunkSearch/ │ │ │ └── node/ │ │ │ └── urlChunkEmbeddingsIndex.ts │ │ ├── workbench/ │ │ │ ├── common/ │ │ │ │ └── workbenchService.ts │ │ │ ├── test/ │ │ │ │ └── vscode-node/ │ │ │ │ └── workbenchServiceImpl.test.ts │ │ │ └── vscode/ │ │ │ └── workbenchServiceImpt.ts │ │ ├── workspace/ │ │ │ ├── common/ │ │ │ │ └── workspaceService.ts │ │ │ └── vscode/ │ │ │ └── workspaceServiceImpl.ts │ │ ├── workspaceChunkSearch/ │ │ │ ├── common/ │ │ │ │ ├── githubAvailableEmbeddingTypes.ts │ │ │ │ ├── rerankerService.ts │ │ │ │ └── workspaceChunkSearch.ts │ │ │ ├── node/ │ │ │ │ ├── codeSearch/ │ │ │ │ │ ├── codeSearchChunkSearch.ts │ │ │ │ │ ├── codeSearchRepo.ts │ │ │ │ │ ├── externalIngestClient.ts │ │ │ │ │ ├── externalIngestIndex.ts │ │ │ │ │ ├── repoTracker.ts │ │ │ │ │ ├── workspaceDiff.ts │ │ │ │ │ └── workspaceFolderIdMap.ts │ │ │ │ ├── embeddingsChunkSearch.ts │ │ │ │ ├── nullWorkspaceFileIndex.ts │ │ │ │ ├── tfidfChunkSearch.ts │ │ │ │ ├── tfidfWithSemanticChunkSearch.ts │ │ │ │ ├── workspaceChunkAndEmbeddingCache.ts │ │ │ │ ├── workspaceChunkEmbeddingsIndex.ts │ │ │ │ ├── workspaceChunkSearchService.ts │ │ │ │ └── workspaceFileIndex.ts │ │ │ └── test/ │ │ │ └── node/ │ │ │ ├── externalIngest.spec.ts │ │ │ ├── isMinified.spec.ts │ │ │ └── workspaceFolderIdMap.spec.ts │ │ ├── workspaceRecorder/ │ │ │ └── common/ │ │ │ ├── resolvedRecording/ │ │ │ │ ├── documentHistory.ts │ │ │ │ ├── operation.ts │ │ │ │ ├── resolvedRecording.ts │ │ │ │ └── sliceRecording.ts │ │ │ └── workspaceLog.ts │ │ └── workspaceState/ │ │ └── common/ │ │ └── promptContextModel.ts │ ├── util/ │ │ ├── common/ │ │ │ ├── annotatedLineRange.ts │ │ │ ├── anomalyDetection.ts │ │ │ ├── arrays.ts │ │ │ ├── async.ts │ │ │ ├── asyncIterableUtils.ts │ │ │ ├── backwardCompatSetting.ts │ │ │ ├── cache.ts │ │ │ ├── chatResponseStreamImpl.ts │ │ │ ├── crypto.ts │ │ │ ├── debounce.ts │ │ │ ├── debugValueEditorGlobals.ts │ │ │ ├── diff.ts │ │ │ ├── errorMessage.ts │ │ │ ├── errors.ts │ │ │ ├── fileSystem.ts │ │ │ ├── fileTree.ts │ │ │ ├── glob.ts │ │ │ ├── globals.d.ts │ │ │ ├── hexdump.ts │ │ │ ├── imageUtils.ts │ │ │ ├── languages.ts │ │ │ ├── lock.ts │ │ │ ├── markdown.ts │ │ │ ├── notebooks.ts │ │ │ ├── pathRedaction.ts │ │ │ ├── progress.ts │ │ │ ├── progressRecorder.ts │ │ │ ├── racePromise.ts │ │ │ ├── range.ts │ │ │ ├── result.ts │ │ │ ├── services.ts │ │ │ ├── taskSingler.ts │ │ │ ├── telemetryCorrelationId.ts │ │ │ ├── test/ │ │ │ │ ├── annotatedSrc.ts │ │ │ │ ├── async.spec.ts │ │ │ │ ├── common/ │ │ │ │ │ └── asyncIterableUtils.spec.ts │ │ │ │ ├── mockChatResponseStream.ts │ │ │ │ ├── notebooks.spec.ts │ │ │ │ ├── result.spec.ts │ │ │ │ ├── shims/ │ │ │ │ │ ├── chatTypes.ts │ │ │ │ │ ├── editing.ts │ │ │ │ │ ├── enums.ts │ │ │ │ │ ├── l10n.ts │ │ │ │ │ ├── newSymbolName.ts │ │ │ │ │ ├── notebookDocument.ts │ │ │ │ │ ├── notebookEditor.ts │ │ │ │ │ ├── terminal.ts │ │ │ │ │ ├── textDocument.ts │ │ │ │ │ ├── textEditor.ts │ │ │ │ │ ├── themes.ts │ │ │ │ │ └── vscodeTypesShim.ts │ │ │ │ ├── simpleMock.ts │ │ │ │ ├── testUtils.spec.ts │ │ │ │ └── testUtils.ts │ │ │ ├── time.ts │ │ │ ├── timeTravelScheduler.ts │ │ │ ├── tokenizer.ts │ │ │ ├── types.ts │ │ │ ├── variableLengthQuantity.ts │ │ │ └── vscodeVersion.ts │ │ ├── node/ │ │ │ ├── crypto.ts │ │ │ ├── jsonFile.ts │ │ │ ├── ports.ts │ │ │ ├── test/ │ │ │ │ ├── anomalyDetection.spec.ts │ │ │ │ ├── debounce.spec.ts │ │ │ │ ├── glob.spec.ts │ │ │ │ ├── lock.spec.ts │ │ │ │ ├── markdown.spec.ts │ │ │ │ └── pathRedaction.spec.ts │ │ │ └── worker.ts │ │ ├── test/ │ │ │ └── node/ │ │ │ ├── errorMessage.spec.ts │ │ │ └── variableLengthQuantity.spec.ts │ │ └── vs/ │ │ ├── base/ │ │ │ ├── common/ │ │ │ │ ├── arrays.ts │ │ │ │ ├── arraysFind.ts │ │ │ │ ├── assert.ts │ │ │ │ ├── async.ts │ │ │ │ ├── buffer.ts │ │ │ │ ├── cache.ts │ │ │ │ ├── cancellation.ts │ │ │ │ ├── charCode.ts │ │ │ │ ├── codicons.ts │ │ │ │ ├── codiconsLibrary.ts │ │ │ │ ├── codiconsUtil.ts │ │ │ │ ├── collections.ts │ │ │ │ ├── date.ts │ │ │ │ ├── diff/ │ │ │ │ │ ├── diff.ts │ │ │ │ │ └── diffChange.ts │ │ │ │ ├── equals.ts │ │ │ │ ├── errors.ts │ │ │ │ ├── event.ts │ │ │ │ ├── extpath.ts │ │ │ │ ├── filters.ts │ │ │ │ ├── functional.ts │ │ │ │ ├── glob.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── htmlContent.ts │ │ │ │ ├── iconLabels.ts │ │ │ │ ├── iterator.ts │ │ │ │ ├── lazy.ts │ │ │ │ ├── lifecycle.ts │ │ │ │ ├── linkedList.ts │ │ │ │ ├── map.ts │ │ │ │ ├── marshallingIds.ts │ │ │ │ ├── mime.ts │ │ │ │ ├── naturalLanguage/ │ │ │ │ │ └── korean.ts │ │ │ │ ├── network.ts │ │ │ │ ├── normalization.ts │ │ │ │ ├── numbers.ts │ │ │ │ ├── objects.ts │ │ │ │ ├── observable.ts │ │ │ │ ├── observableInternal/ │ │ │ │ │ ├── base.ts │ │ │ │ │ ├── changeTracker.ts │ │ │ │ │ ├── commonFacade/ │ │ │ │ │ │ ├── cancellation.ts │ │ │ │ │ │ └── deps.ts │ │ │ │ │ ├── debugLocation.ts │ │ │ │ │ ├── debugName.ts │ │ │ │ │ ├── experimental/ │ │ │ │ │ │ └── utils.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logging/ │ │ │ │ │ │ ├── consoleObservableLogger.ts │ │ │ │ │ │ ├── debugGetDependencyGraph.ts │ │ │ │ │ │ ├── debugger/ │ │ │ │ │ │ │ ├── debuggerApi.d.ts │ │ │ │ │ │ │ ├── debuggerRpc.ts │ │ │ │ │ │ │ ├── devToolsLogger.ts │ │ │ │ │ │ │ ├── rpc.ts │ │ │ │ │ │ │ └── utils.ts │ │ │ │ │ │ └── logging.ts │ │ │ │ │ ├── map.ts │ │ │ │ │ ├── observables/ │ │ │ │ │ │ ├── baseObservable.ts │ │ │ │ │ │ ├── constObservable.ts │ │ │ │ │ │ ├── derived.ts │ │ │ │ │ │ ├── derivedImpl.ts │ │ │ │ │ │ ├── lazyObservableValue.ts │ │ │ │ │ │ ├── observableFromEvent.ts │ │ │ │ │ │ ├── observableSignal.ts │ │ │ │ │ │ ├── observableSignalFromEvent.ts │ │ │ │ │ │ ├── observableValue.ts │ │ │ │ │ │ └── observableValueOpts.ts │ │ │ │ │ ├── reactions/ │ │ │ │ │ │ ├── autorun.ts │ │ │ │ │ │ └── autorunImpl.ts │ │ │ │ │ ├── set.ts │ │ │ │ │ ├── transaction.ts │ │ │ │ │ └── utils/ │ │ │ │ │ ├── promise.ts │ │ │ │ │ ├── runOnChange.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ ├── utilsCancellation.ts │ │ │ │ │ └── valueWithChangeEvent.ts │ │ │ │ ├── path.ts │ │ │ │ ├── platform.ts │ │ │ │ ├── process.ts │ │ │ │ ├── resources.ts │ │ │ │ ├── sequence.ts │ │ │ │ ├── sseParser.ts │ │ │ │ ├── stopwatch.ts │ │ │ │ ├── stream.ts │ │ │ │ ├── strings.ts │ │ │ │ ├── symbols.ts │ │ │ │ ├── ternarySearchTree.ts │ │ │ │ ├── themables.ts │ │ │ │ ├── types.ts │ │ │ │ ├── uint.ts │ │ │ │ ├── uri.ts │ │ │ │ ├── uuid.ts │ │ │ │ └── yaml.ts │ │ │ └── node/ │ │ │ └── ports.ts │ │ ├── base-common.d.ts │ │ ├── crypto.d.ts │ │ ├── editor/ │ │ │ └── common/ │ │ │ ├── core/ │ │ │ │ ├── editOperation.ts │ │ │ │ ├── edits/ │ │ │ │ │ ├── arrayEdit.ts │ │ │ │ │ ├── edit.ts │ │ │ │ │ ├── lengthEdit.ts │ │ │ │ │ ├── lineEdit.ts │ │ │ │ │ ├── stringEdit.ts │ │ │ │ │ └── textEdit.ts │ │ │ │ ├── position.ts │ │ │ │ ├── range.ts │ │ │ │ ├── ranges/ │ │ │ │ │ ├── lineRange.ts │ │ │ │ │ └── offsetRange.ts │ │ │ │ ├── text/ │ │ │ │ │ ├── abstractText.ts │ │ │ │ │ ├── positionToOffset.ts │ │ │ │ │ ├── positionToOffsetImpl.ts │ │ │ │ │ └── textLength.ts │ │ │ │ └── wordHelper.ts │ │ │ ├── diff/ │ │ │ │ ├── defaultLinesDiffComputer/ │ │ │ │ │ ├── algorithms/ │ │ │ │ │ │ ├── diffAlgorithm.ts │ │ │ │ │ │ ├── dynamicProgrammingDiffing.ts │ │ │ │ │ │ └── myersDiffAlgorithm.ts │ │ │ │ │ ├── computeMovedLines.ts │ │ │ │ │ ├── defaultLinesDiffComputer.ts │ │ │ │ │ ├── heuristicSequenceOptimizations.ts │ │ │ │ │ ├── lineSequence.ts │ │ │ │ │ ├── linesSliceCharSequence.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── legacyLinesDiffComputer.ts │ │ │ │ ├── linesDiffComputer.ts │ │ │ │ └── rangeMapping.ts │ │ │ └── model/ │ │ │ ├── mirrorTextModel.ts │ │ │ └── prefixSumComputer.ts │ │ ├── nls.ts │ │ ├── platform/ │ │ │ └── instantiation/ │ │ │ └── common/ │ │ │ ├── descriptors.ts │ │ │ ├── graph.ts │ │ │ ├── instantiation.ts │ │ │ ├── instantiationService.ts │ │ │ └── serviceCollection.ts │ │ ├── vscode-globals-nls.d.ts │ │ ├── vscode-globals-product.d.ts │ │ └── workbench/ │ │ ├── api/ │ │ │ └── common/ │ │ │ ├── extHostDocumentData.ts │ │ │ └── extHostTypes/ │ │ │ ├── diagnostic.ts │ │ │ ├── es5ClassCompat.ts │ │ │ ├── location.ts │ │ │ ├── markdownString.ts │ │ │ ├── notebooks.ts │ │ │ ├── position.ts │ │ │ ├── range.ts │ │ │ ├── selection.ts │ │ │ ├── snippetString.ts │ │ │ ├── snippetTextEdit.ts │ │ │ ├── symbolInformation.ts │ │ │ └── textEdit.ts │ │ └── contrib/ │ │ └── chat/ │ │ └── common/ │ │ └── promptSyntax/ │ │ └── promptFileParser.ts │ └── vscodeTypes.ts ├── test/ │ ├── base/ │ │ ├── cache-cli.ts │ │ ├── cache.ts │ │ ├── cachingChatMLFetcher.ts │ │ ├── cachingChunksEndpointClient.ts │ │ ├── cachingCodeSearchClient.ts │ │ ├── cachingCompletionsFetchService.ts │ │ ├── cachingEmbeddingsFetcher.ts │ │ ├── cachingResourceFetcher.ts │ │ ├── chatMLCache.ts │ │ ├── completionsCache.ts │ │ ├── embeddingsCache.ts │ │ ├── extHostContext/ │ │ │ ├── simulationExtHostContext.ts │ │ │ ├── simulationExtHostToolsService.ts │ │ │ └── simulationWorkspaceExtHost.ts │ │ ├── fileUtils.ts │ │ ├── hash.ts │ │ ├── pausableThrottledWorker.ts │ │ ├── rubric.ts │ │ ├── salts.ts │ │ ├── simulationBaseline.ts │ │ ├── simulationContext.ts │ │ ├── simulationEndpointHealth.ts │ │ ├── simulationOptions.ts │ │ ├── simulationOutcome.ts │ │ ├── simuliationWorkspaceChunkSearch.ts │ │ ├── spyingChatMLFetcher.ts │ │ ├── stdout.ts │ │ ├── stest.ts │ │ ├── throttlingChatMLFetcher.ts │ │ ├── throttlingCodeOrDocsSearchClient.ts │ │ └── validate.ts │ ├── cacheSalt.ts │ ├── codeMapper/ │ │ └── codeMapper.stest.ts │ ├── e2e/ │ │ ├── cli.stest.ts │ │ ├── edit.stest.ts │ │ ├── evaluators/ │ │ │ └── pythonFix.ts │ │ ├── explain.stest.ts │ │ ├── fetchWebPageTool.stest.ts │ │ ├── findFilesTool.stest.ts │ │ ├── markdown.stest.ts │ │ ├── newNotebook.stest.ts │ │ ├── newWorkspace.stest.ts │ │ ├── notebookTools.stest.ts │ │ ├── pythonFix.stest.ts │ │ ├── scenarioLoader.ts │ │ ├── scenarioTest.ts │ │ ├── search.stest.ts │ │ ├── semanticSearch.stest.ts │ │ ├── semanticSearchView.stest.ts │ │ ├── system.stest.ts │ │ ├── terminal.stest.ts │ │ ├── testHelper.ts │ │ ├── toolSimTest.ts │ │ ├── tools.stest.ts │ │ ├── typescriptFix.stest.ts │ │ ├── variables.stest.ts │ │ ├── vscode-metaprompt.stest.ts │ │ ├── vscode.stest.ts │ │ ├── workspace-e2e.stest.ts │ │ └── workspace-metaprompt.stest.ts │ ├── inline/ │ │ ├── agent.stest.ts │ │ ├── fixing.stest.ts │ │ ├── inlineEditCode.stest.ts │ │ ├── inlineExplain.stest.ts │ │ ├── inlineGenerateCode.stest.ts │ │ ├── multiFileEdit.stest.ts │ │ ├── review.stest.ts │ │ ├── slashDoc.cpp.stest.ts │ │ ├── slashDoc.java.stest.ts │ │ ├── slashDoc.py.stest.ts │ │ ├── slashDoc.rb.stest.ts │ │ ├── slashDoc.ts.stest.ts │ │ ├── slashDoc.util.ts │ │ └── test/ │ │ └── assertPyDocstring.spec.ts │ ├── intent/ │ │ ├── inline-chat.json │ │ ├── inlineChatIntent.stest.ts │ │ ├── intentTest.ts │ │ ├── panel-chat-github.json │ │ ├── panel-chat-unknown.json │ │ ├── panel-chat.json │ │ └── panelChatIntent.stest.ts │ ├── jsonOutputPrinter.ts │ ├── outcome/ │ │ ├── -doc-inline.json │ │ ├── -review-inline.json │ │ ├── -tests-custom-instructions-inline.json │ │ ├── -tests-inline.json │ │ ├── -tests-panel.json │ │ ├── -tests-real-world-inline.json │ │ ├── codemapper-context.json │ │ ├── custom-instructions-inline.json │ │ ├── debug-config-to-command-context.json │ │ ├── debug-tools-list-context.json │ │ ├── dev-container-configuration-external.json │ │ ├── edit-inline.json │ │ ├── edit-inlinechatintent-inline.json │ │ ├── explain-expanded-context-panel.json │ │ ├── explain-inline.json │ │ ├── fix-cpp-inline.json │ │ ├── fix-eslint-inline.json │ │ ├── fix-inlinechatintent-cpp-inline.json │ │ ├── fix-inlinechatintent-eslint-inline.json │ │ ├── fix-inlinechatintent-powershell-inline.json │ │ ├── fix-inlinechatintent-pylint-inline.json │ │ ├── fix-inlinechatintent-pyright-inline.json │ │ ├── fix-inlinechatintent-roslyn-inline.json │ │ ├── fix-inlinechatintent-ruff-inline.json │ │ ├── fix-inlinechatintent-tsc-inline.json │ │ ├── fix-powershell-inline.json │ │ ├── fix-pylint-inline.json │ │ ├── fix-pyright-inline.json │ │ ├── fix-python-panel.json │ │ ├── fix-roslyn-inline.json │ │ ├── fix-ruff-inline.json │ │ ├── fix-tsc-inline.json │ │ ├── fix-typescript-panel.json │ │ ├── generate-inline.json │ │ ├── generate-inlinechatintent-inline.json │ │ ├── generate-markdown-panel.json │ │ ├── git-commit-message-external.json │ │ ├── inlineedit-goldenscenario-xtab-external.json │ │ ├── intent-inline.json │ │ ├── intent-panel.json │ │ ├── multifile-edit-claude-panel.json │ │ ├── multifile-edit-panel.json │ │ ├── new-prompt-panel.json │ │ ├── newnotebook-prompt-panel.json │ │ ├── notebook-edit-inline.json │ │ ├── notebook-fix-inline.json │ │ ├── notebook-fix-runtime-inline.json │ │ ├── notebook-generate-inline.json │ │ ├── notebook-generate-runtime-inline.json │ │ ├── notebookedits-bug-reports-json-panel.json │ │ ├── notebookedits-bug-reports-text-panel.json │ │ ├── notebookedits-bug-reports-xml-panel.json │ │ ├── notebookedits-modification-json-panel.json │ │ ├── notebookedits-modification-text-panel.json │ │ ├── notebookedits-modification-xml-panel.json │ │ ├── pr-title-and-description-context.json │ │ ├── rename-suggestions-external.json │ │ ├── search-panel.json │ │ ├── settingseditorsearchresultsselector-external.json │ │ ├── setuptests-invoke-panel.json │ │ ├── setuptests-recommend-panel.json │ │ ├── system-identity-panel.json │ │ ├── terminal-general-panel.json │ │ ├── terminal-git-panel.json │ │ └── variables-panel.json │ ├── outputColorer.ts │ ├── prompts/ │ │ ├── customInstructions.stest.ts │ │ ├── devContainerConfigGenerator.stest.ts │ │ ├── fixtures/ │ │ │ └── devcontainer/ │ │ │ ├── devContainerConfigTestData.json │ │ │ └── devContainerIndex.json │ │ ├── gitCommitMessageGenerator.stest.ts │ │ ├── newNotebookCell.stest.ts │ │ ├── newWorkspace.stest.ts │ │ └── settingsEditorSearchResultsSelector.stest.ts │ ├── requirements.txt │ ├── scenarios/ │ │ ├── test-cli/ │ │ │ ├── wkspc1/ │ │ │ │ ├── demo.py │ │ │ │ ├── sample.js │ │ │ │ ├── stringUtils.js │ │ │ │ └── utils.js │ │ │ └── wkspc2/ │ │ │ └── foobar.js │ │ ├── test-current-selection-impls/ │ │ │ ├── test.ts │ │ │ ├── workspaceState.state.json │ │ │ ├── workspaceState1.state.json │ │ │ └── workspaceState2.state.json │ │ ├── test-explain/ │ │ │ ├── bar.ts │ │ │ ├── baz.ts │ │ │ ├── classes.py │ │ │ ├── classes.rb │ │ │ ├── classes.ts │ │ │ ├── cursor.ts │ │ │ ├── explain.0.conversation.json │ │ │ ├── explain.0.state.json │ │ │ ├── explain.1.conversation.json │ │ │ ├── explain.1.state.json │ │ │ ├── explain.10.conversation.json │ │ │ ├── explain.10.state.json │ │ │ ├── explain.11.conversation.json │ │ │ ├── explain.11.state.json │ │ │ ├── explain.12.conversation.json │ │ │ ├── explain.12.state.json │ │ │ ├── explain.13.conversation.json │ │ │ ├── explain.13.state.json │ │ │ ├── explain.15.conversation.json │ │ │ ├── explain.15.state.json │ │ │ ├── explain.16.conversation.json │ │ │ ├── explain.16.state.json │ │ │ ├── explain.2.conversation.json │ │ │ ├── explain.2.state.json │ │ │ ├── explain.3.conversation.json │ │ │ ├── explain.3.state.json │ │ │ ├── explain.4.conversation.json │ │ │ ├── explain.5.conversation.json │ │ │ ├── explain.5.state.json │ │ │ ├── explain.6.conversation.json │ │ │ ├── explain.6.state.json │ │ │ ├── explain.7.conversation.json │ │ │ ├── explain.7.state.json │ │ │ ├── explain.8.conversation.json │ │ │ ├── explain.8.state.json │ │ │ ├── explain.9.conversation.json │ │ │ ├── explain.9.state.json │ │ │ ├── foo.ts │ │ │ ├── functions.cpp │ │ │ ├── functions.cs │ │ │ ├── functions.go │ │ │ ├── functions.py │ │ │ ├── functions.rb │ │ │ ├── functions.ts │ │ │ ├── methods.java │ │ │ ├── methods.ts │ │ │ └── types.ts │ │ ├── test-generate-markdown/ │ │ │ ├── file.ts │ │ │ ├── test0.conversation.json │ │ │ └── workspaceState.state.json │ │ ├── test-new-notebooks/ │ │ │ ├── fib.md │ │ │ ├── test0.conversation.json │ │ │ └── workspaceState.state.json │ │ ├── test-new-workspace/ │ │ │ ├── fib.md │ │ │ ├── functions.ts │ │ │ ├── newWorkspace0.conversation.json │ │ │ ├── newWorkspace1.conversation.json │ │ │ ├── newWorkspace2.conversation.json │ │ │ ├── newWorkspace3.conversation.json │ │ │ ├── newWorkspace4.conversation.json │ │ │ ├── newWorkspace5.conversation.json │ │ │ ├── newWorkspace6.conversation.json │ │ │ ├── newWorkspace7.conversation.json │ │ │ ├── newWorkspace8.conversation.json │ │ │ ├── newWorkspace9.conversation.json │ │ │ ├── workspaceState.9.state.json │ │ │ └── workspaceState.state.json │ │ ├── test-notebook-tools/ │ │ │ ├── Chipotle.solution.ipynb │ │ │ ├── Chipotle1.state.json │ │ │ └── LICENSE │ │ ├── test-notebooks/ │ │ │ ├── Chipotle.conversation.json │ │ │ ├── Chipotle.solution.ipynb │ │ │ ├── Chipotle1.state.json │ │ │ ├── Chipotle10.state.json │ │ │ ├── Chipotle11.state.json │ │ │ ├── Chipotle12.state.json │ │ │ ├── Chipotle13.state.json │ │ │ ├── Chipotle14.state.json │ │ │ ├── Chipotle15.state.json │ │ │ ├── Chipotle16.state.json │ │ │ ├── Chipotle2.state.json │ │ │ ├── Chipotle3.state.json │ │ │ ├── Chipotle4.state.json │ │ │ ├── Chipotle5.state.json │ │ │ ├── Chipotle6.state.json │ │ │ ├── Chipotle7.state.json │ │ │ ├── Chipotle8.state.json │ │ │ ├── Chipotle9.state.json │ │ │ └── LICENSE │ │ ├── test-scenario-1/ │ │ │ ├── bar.js │ │ │ ├── emptySelection.state.json │ │ │ ├── fib.js │ │ │ ├── test1.conversation.json │ │ │ ├── test2.conversation.json │ │ │ └── workspaceState.state.json │ │ ├── test-scenario-fix-python/ │ │ │ ├── case1.conversation.json │ │ │ ├── case1.py │ │ │ ├── case1.state.json │ │ │ ├── case10.conversation.json │ │ │ ├── case10.py │ │ │ ├── case10.state.json │ │ │ ├── case2.conversation.json │ │ │ ├── case2.py │ │ │ ├── case2.state.json │ │ │ ├── case3.conversation.json │ │ │ ├── case3.py │ │ │ ├── case3.state.json │ │ │ ├── case4.conversation.json │ │ │ ├── case4.py │ │ │ ├── case4.state.json │ │ │ ├── case5.conversation.json │ │ │ ├── case5.py │ │ │ ├── case5.state.json │ │ │ ├── case6.conversation.json │ │ │ ├── case6.py │ │ │ ├── case6.state.json │ │ │ ├── case7.conversation.json │ │ │ ├── case7.py │ │ │ ├── case7.state.json │ │ │ ├── case8.conversation.json │ │ │ ├── case8.py │ │ │ ├── case8.state.json │ │ │ ├── case9.conversation.json │ │ │ ├── case9.py │ │ │ └── case9.state.json │ │ ├── test-scenario-fix-typescript/ │ │ │ ├── file1.ts │ │ │ ├── file2.ts │ │ │ ├── fix-implements-typescript.conversation.json │ │ │ └── implements.state.json │ │ ├── test-scenario-search/ │ │ │ ├── example-files/ │ │ │ │ ├── bar.html │ │ │ │ ├── example.ts │ │ │ │ ├── foo.md │ │ │ │ ├── style.css │ │ │ │ └── test.py │ │ │ ├── replace-samples/ │ │ │ │ ├── foo.replace2.md │ │ │ │ └── style.replace17.css │ │ │ ├── search0.testArgs.json │ │ │ ├── search1.testArgs.json │ │ │ ├── search10.testArgs.json │ │ │ ├── search11.testArgs.json │ │ │ ├── search12.testArgs.json │ │ │ ├── search13.testArgs.json │ │ │ ├── search14.testArgs.json │ │ │ ├── search15.testArgs.json │ │ │ ├── search16.testArgs.json │ │ │ ├── search17.testArgs.json │ │ │ ├── search18.testArgs.json │ │ │ ├── search19.testArgs.json │ │ │ ├── search2.testArgs.json │ │ │ ├── search20.testArgs.json │ │ │ ├── search21.testArgs.json │ │ │ ├── search22.testArgs.json │ │ │ ├── search23.testArgs.json │ │ │ ├── search24.testArgs.json │ │ │ ├── search25.testArgs.json │ │ │ ├── search3.testArgs.json │ │ │ ├── search4.testArgs.json │ │ │ ├── search5.testArgs.json │ │ │ ├── search6.testArgs.json │ │ │ ├── search7.testArgs.json │ │ │ ├── search8.testArgs.json │ │ │ └── search9.testArgs.json │ │ ├── test-semantic-search/ │ │ │ ├── workspace0.conversation.json │ │ │ ├── workspace1.conversation.json │ │ │ ├── workspaceState.state.json │ │ │ └── workspaceState1.state.json │ │ ├── test-setupTest/ │ │ │ ├── testSetup.0.conversation.json │ │ │ ├── testSetup.1.conversation.json │ │ │ ├── testSetup.2.conversation.json │ │ │ ├── testSetup.3.conversation.json │ │ │ ├── testSetup.4.conversation.json │ │ │ ├── testSetup.5.conversation.json │ │ │ └── testSetup.6.conversation.json │ │ ├── test-setupTestRecommend/ │ │ │ ├── testSetup.0.conversation.json │ │ │ ├── testSetup.1.conversation.json │ │ │ ├── testSetup.2.conversation.json │ │ │ ├── testSetup.3.conversation.json │ │ │ ├── testSetup.4.conversation.json │ │ │ ├── testSetup.5.conversation.json │ │ │ ├── testSetup.6.conversation.json │ │ │ ├── testSetup.7.conversation.json │ │ │ └── testSetup.8.conversation.json │ │ ├── test-system/ │ │ │ ├── fib.md │ │ │ ├── puppeteer.js │ │ │ ├── system.5.conversation.json │ │ │ ├── system.6.conversation.json │ │ │ ├── system0.conversation.json │ │ │ ├── system1.conversation.json │ │ │ ├── system2.conversation.json │ │ │ ├── system3.conversation.json │ │ │ ├── system4.conversation.json │ │ │ ├── workspaceState.state.json │ │ │ └── workspaceState3.state.json │ │ ├── test-terminal/ │ │ │ ├── bash.state.json │ │ │ ├── fish.state.json │ │ │ ├── powershell.state.json │ │ │ └── zsh.state.json │ │ ├── test-tools/ │ │ │ ├── chatSetup.state.json │ │ │ ├── tools.0.conversation.json │ │ │ ├── tools.1.conversation.json │ │ │ ├── tools.state.json │ │ │ └── workspace/ │ │ │ ├── chatSetup.ts │ │ │ ├── file.md │ │ │ └── functions.ts │ │ └── test-variables/ │ │ ├── functions.ts │ │ ├── variables.0.conversation.json │ │ ├── variables.0.state.json │ │ ├── variables.1.conversation.json │ │ └── variables.1.state.json │ ├── simulation/ │ │ ├── baseline.json │ │ ├── baseline.old.json │ │ ├── debugCommandToConfig.stest.ts │ │ ├── debugTools.stest.ts │ │ ├── diagnosticProviders/ │ │ │ ├── cpp.ts │ │ │ ├── diagnosticsProvider.ts │ │ │ ├── eslint.ts │ │ │ ├── index.ts │ │ │ ├── python.ts │ │ │ ├── roslyn.ts │ │ │ ├── ruff.ts │ │ │ ├── tsc.ts │ │ │ └── utils.ts │ │ ├── externalScenarios.ts │ │ ├── fixtures/ │ │ │ ├── codeMapper/ │ │ │ │ ├── extHostExtensionActivator.test.ts │ │ │ │ ├── fibonacci_iterative.ts │ │ │ │ ├── fibonacci_recursive.ts │ │ │ │ ├── index.ts │ │ │ │ ├── notebookEditorWidget.ts │ │ │ │ ├── package.json │ │ │ │ ├── product-build-linux.yml │ │ │ │ ├── product-icons-mixed.md │ │ │ │ ├── product-icons-sorted.md │ │ │ │ ├── quickInput.ts │ │ │ │ ├── scanner.ts │ │ │ │ └── scannerTypes.ts │ │ │ ├── cpp/ │ │ │ │ ├── basic/ │ │ │ │ │ └── main.cpp │ │ │ │ └── headers/ │ │ │ │ ├── abi_macros.hpp │ │ │ │ └── json_fwd.hpp │ │ │ ├── doc/ │ │ │ │ └── issue-6406/ │ │ │ │ └── debugModel.ts │ │ │ ├── doc-everywhere-issue-3763/ │ │ │ │ └── githubServer.ts │ │ │ ├── doc-explain-ts-code/ │ │ │ │ ├── charCode.ts │ │ │ │ └── strings.ts │ │ │ ├── doc-hello-world/ │ │ │ │ └── colors.ts │ │ │ ├── doc-ruby/ │ │ │ │ └── fib.rb │ │ │ ├── doc-ts-class/ │ │ │ │ └── keybindingResolver.ts │ │ │ ├── doc-ts-class-full/ │ │ │ │ └── keybindingResolver.ts │ │ │ ├── doc-ts-interface/ │ │ │ │ └── codeImportPatterns.ts │ │ │ ├── doc-ts-large-fn/ │ │ │ │ └── resolver.ts │ │ │ ├── edit/ │ │ │ │ ├── 3575.ts │ │ │ │ ├── 4302.ts │ │ │ │ ├── 5710.ts │ │ │ │ ├── 6276.ts │ │ │ │ ├── issue-2988/ │ │ │ │ │ └── pseudoStartStopConversationCallback.test.ts │ │ │ │ ├── issue-5755/ │ │ │ │ │ └── vscode.proposed.chatParticipantAdditions.d.ts │ │ │ │ ├── issue-6059/ │ │ │ │ │ └── serializers.ts │ │ │ │ ├── issue-6329/ │ │ │ │ │ └── math.js │ │ │ │ ├── issue-6469/ │ │ │ │ │ └── inlineChat.css │ │ │ │ ├── issue-6614/ │ │ │ │ │ └── workbench-dev.html │ │ │ │ ├── issue-6973/ │ │ │ │ │ └── utils.ts │ │ │ │ ├── issue-7202/ │ │ │ │ │ └── languageModelToolsContribution.ts │ │ │ │ ├── issue-7282/ │ │ │ │ │ └── math.js │ │ │ │ ├── issue-7487/ │ │ │ │ │ └── EditForm.tsx │ │ │ │ ├── issue-7996/ │ │ │ │ │ └── codeEditorWidget.ts │ │ │ │ ├── issue-8129/ │ │ │ │ │ └── optimize.ts │ │ │ │ ├── issue-release-142/ │ │ │ │ │ └── testAuthProvider.ts │ │ │ │ ├── issue-release-275/ │ │ │ │ │ └── BasketService.cs │ │ │ │ └── markdown/ │ │ │ │ ├── README.md │ │ │ │ └── explanation.md │ │ │ ├── edit-add-enum-variant/ │ │ │ │ └── index.ts │ │ │ ├── edit-add-explicit-type-issue-3759/ │ │ │ │ └── pullRequestModel.ts │ │ │ ├── edit-add-toString/ │ │ │ │ └── index.ts │ │ │ ├── edit-add-toString2/ │ │ │ │ └── index.ts │ │ │ ├── edit-asyncawait-4151/ │ │ │ │ └── index.ts │ │ │ ├── edit-convert-ternary-to-if-else/ │ │ │ │ └── index.ts │ │ │ ├── edit-import-assert/ │ │ │ │ └── index.ts │ │ │ ├── edit-import-assert2/ │ │ │ │ └── index.ts │ │ │ ├── edit-issue-1198/ │ │ │ │ └── main.py │ │ │ ├── edit-refactor-loop/ │ │ │ │ └── index.ts │ │ │ ├── edit-single-line-await-issue-3702/ │ │ │ │ └── interactiveEditorWidget.ts │ │ │ ├── edit-slice-4149/ │ │ │ │ └── index.ts │ │ │ ├── editing/ │ │ │ │ ├── mainThreadChatAgents2.ts │ │ │ │ └── math.js │ │ │ ├── editing-html/ │ │ │ │ └── index.html │ │ │ ├── explain-project-context/ │ │ │ │ ├── inlineChat.css │ │ │ │ ├── package.json │ │ │ │ └── tsconfig.json │ │ │ ├── fix/ │ │ │ │ ├── issue-7544/ │ │ │ │ │ └── notebookMulticursor.ts │ │ │ │ └── issue-7894/ │ │ │ │ └── shellIntegration.ps1 │ │ │ ├── fixing/ │ │ │ │ ├── cpp/ │ │ │ │ │ ├── basic/ │ │ │ │ │ │ └── main.cpp │ │ │ │ │ └── headers/ │ │ │ │ │ ├── abi_macros.hpp │ │ │ │ │ └── json_fwd.hpp │ │ │ │ ├── csharp/ │ │ │ │ │ ├── roslyn_call_not_awaited.cs │ │ │ │ │ ├── roslyn_does_not_contain_definition_for.cs │ │ │ │ │ ├── roslyn_does_not_exist.cs │ │ │ │ │ ├── roslyn_field_never_used.cs │ │ │ │ │ ├── roslyn_has_same_name_as.cs │ │ │ │ │ ├── roslyn_missing_using_directive.cs │ │ │ │ │ ├── roslyn_no_argument_given.cs │ │ │ │ │ └── roslyn_semi_colon_expected.cs │ │ │ │ ├── python/ │ │ │ │ │ ├── pylint_line_too_long_1.py │ │ │ │ │ ├── pylint_line_too_long_2.py │ │ │ │ │ ├── pylint_line_too_long_3.py │ │ │ │ │ ├── pylint_line_too_long_4.py │ │ │ │ │ ├── pylint_line_too_long_5.py │ │ │ │ │ ├── pylint_unecessary_parenthesis.py │ │ │ │ │ ├── pylint_unused_import.py │ │ │ │ │ ├── pyright_annotated_types_missing_argument.py │ │ │ │ │ ├── pyright_assignment_scopes.py │ │ │ │ │ ├── pyright_async_in_non_async_function.py │ │ │ │ │ ├── pyright_await_in_non_async_function.py │ │ │ │ │ ├── pyright_badtoken.py │ │ │ │ │ ├── pyright_can_not_access_member.py │ │ │ │ │ ├── pyright_can_not_be_assigned_to_1.py │ │ │ │ │ ├── pyright_can_not_be_assigned_to_2.py │ │ │ │ │ ├── pyright_can_not_be_assigned_to_3.py │ │ │ │ │ ├── pyright_general_type_issue.py │ │ │ │ │ ├── pyright_missing_import.py │ │ │ │ │ ├── pyright_missing_method.py │ │ │ │ │ ├── pyright_no_abstract_class_instantiation.py │ │ │ │ │ ├── pyright_no_to_string_member.py │ │ │ │ │ ├── pyright_no_value_for_argument.py │ │ │ │ │ ├── pyright_not_defined.py │ │ │ │ │ ├── pyright_object_not_subscriptable.py │ │ │ │ │ ├── pyright_optional_member_access.py │ │ │ │ │ ├── pyright_parameter_already_assigned.py │ │ │ │ │ ├── pyright_self_as_first_argument.py │ │ │ │ │ ├── pyright_unbound_variable.py │ │ │ │ │ └── pyright_undefined_variable.py │ │ │ │ ├── ruff/ │ │ │ │ │ └── ruff_error_E231.py │ │ │ │ └── typescript/ │ │ │ │ ├── eslint_class_methods_use_this.ts │ │ │ │ ├── eslint_comma_expected.ts │ │ │ │ ├── eslint_consistent_this.ts │ │ │ │ ├── eslint_constructor_super.ts │ │ │ │ ├── eslint_do_not_access_hasOwnProperty.ts │ │ │ │ ├── eslint_expected_conditional_expression.ts │ │ │ │ ├── eslint_func_names.ts │ │ │ │ ├── eslint_func_style.ts │ │ │ │ ├── eslint_max_lines_per_function.ts │ │ │ │ ├── eslint_max_params.ts │ │ │ │ ├── eslint_max_statements.ts │ │ │ │ ├── eslint_no_case_declarations.ts │ │ │ │ ├── eslint_no_dupe_else_if.ts │ │ │ │ ├── eslint_no_duplicate_case.ts │ │ │ │ ├── eslint_no_duplicate_imports.ts │ │ │ │ ├── eslint_no_fallthrough.ts │ │ │ │ ├── eslint_no_inner_declarations.ts │ │ │ │ ├── eslint_no_multi_assign.ts │ │ │ │ ├── eslint_no_negated_condition.ts │ │ │ │ ├── eslint_no_negated_condition_2.ts │ │ │ │ ├── eslint_no_new.ts │ │ │ │ ├── eslint_no_sequences.ts │ │ │ │ ├── eslint_no_sparse_arrays.ts │ │ │ │ ├── eslint_no_sparse_arrays_2.ts │ │ │ │ ├── eslint_no_sparse_arrays_3.ts │ │ │ │ ├── eslint_require_await.ts │ │ │ │ ├── eslint_sort_keys.ts │ │ │ │ ├── eslint_unexpected_constant_condition_1.ts │ │ │ │ ├── eslint_unexpected_constant_condition_2.ts │ │ │ │ ├── eslint_unexpected_control_character.ts │ │ │ │ ├── eslint_unexpected_token.ts │ │ │ │ ├── eslint_unreachable_code.ts │ │ │ │ ├── inlineChatSimulator.ts │ │ │ │ ├── tsc_error_1015.ts │ │ │ │ ├── tsc_error_1128.ts │ │ │ │ ├── tsc_error_18047.ts │ │ │ │ ├── tsc_error_18048.ts │ │ │ │ ├── tsc_error_2300.ts │ │ │ │ ├── tsc_error_2304/ │ │ │ │ │ ├── file0.ts │ │ │ │ │ └── file1.ts │ │ │ │ ├── tsc_error_2304_1.ts │ │ │ │ ├── tsc_error_2304_2.ts │ │ │ │ ├── tsc_error_2304_3.ts │ │ │ │ ├── tsc_error_2307_can_not_find_module.ts │ │ │ │ ├── tsc_error_2322.ts │ │ │ │ ├── tsc_error_2339_1.ts │ │ │ │ ├── tsc_error_2339_2.ts │ │ │ │ ├── tsc_error_2339_3.ts │ │ │ │ ├── tsc_error_2339_4.ts │ │ │ │ ├── tsc_error_2341.ts │ │ │ │ ├── tsc_error_2345.ts │ │ │ │ ├── tsc_error_2345_2/ │ │ │ │ │ ├── database_mock.ts │ │ │ │ │ ├── file0.ts │ │ │ │ │ └── file1.ts │ │ │ │ ├── tsc_error_2345_3/ │ │ │ │ │ ├── database_mock.ts │ │ │ │ │ └── file0.ts │ │ │ │ ├── tsc_error_2355.ts │ │ │ │ ├── tsc_error_2391.ts │ │ │ │ ├── tsc_error_2420/ │ │ │ │ │ ├── file0.ts │ │ │ │ │ └── file1.ts │ │ │ │ ├── tsc_error_2454.ts │ │ │ │ ├── tsc_error_2532.ts │ │ │ │ ├── tsc_error_2554/ │ │ │ │ │ ├── database_mock.ts │ │ │ │ │ ├── file1.ts │ │ │ │ │ └── legacy_database.ts │ │ │ │ ├── tsc_error_2554.ts │ │ │ │ ├── tsc_error_2695.ts │ │ │ │ ├── tsc_error_7006.ts │ │ │ │ ├── tsc_error_7053.ts │ │ │ │ ├── tsc_implicit_any.ts │ │ │ │ └── tsc_large_onigscanner/ │ │ │ │ ├── tsc_error_2802.ts │ │ │ │ └── tsc_error_2802_tsconfig.json │ │ │ ├── gen/ │ │ │ │ ├── 4080.ts │ │ │ │ ├── 4179.ts │ │ │ │ ├── 5439.py │ │ │ │ ├── 6234/ │ │ │ │ │ └── top-packages.ts │ │ │ │ ├── 6554/ │ │ │ │ │ └── update-vs-base.ps1 │ │ │ │ ├── CHANGELOG.md │ │ │ │ ├── commandCenterControl.ts │ │ │ │ ├── inlayHintsController.ts │ │ │ │ ├── keybindingParser.ts │ │ │ │ ├── modelLines.ts │ │ │ │ ├── strings.ts │ │ │ │ └── variables/ │ │ │ │ ├── example.ts │ │ │ │ └── output.ts │ │ │ ├── gen-json/ │ │ │ │ └── test.json │ │ │ ├── gen-method-issue-3602/ │ │ │ │ └── editor.ts │ │ │ ├── gen-nestjs-route-issue-3604/ │ │ │ │ └── app.controller.ts │ │ │ ├── gen-python-palindrome/ │ │ │ │ └── new.py │ │ │ ├── gen-top-level-function/ │ │ │ │ ├── charCode.ts │ │ │ │ └── strings.ts │ │ │ ├── gen-twice-issue-3597/ │ │ │ │ └── new.js │ │ │ ├── generate/ │ │ │ │ ├── issue-6163/ │ │ │ │ │ └── package.json │ │ │ │ ├── issue-6505/ │ │ │ │ │ └── chatParserTypes.ts │ │ │ │ ├── issue-6788/ │ │ │ │ │ └── terminalSuggestAddon.ts │ │ │ │ ├── issue-6956/ │ │ │ │ │ └── .eslintrc.js │ │ │ │ ├── issue-7088/ │ │ │ │ │ └── Microsoft.PowerShell_profile.ps1 │ │ │ │ └── issue-7772/ │ │ │ │ └── builds.ts │ │ │ ├── ghpr/ │ │ │ │ └── commands.ts │ │ │ ├── inlineEdits/ │ │ │ │ ├── 1-point.ts/ │ │ │ │ │ ├── recording.w.json │ │ │ │ │ └── scoredEdits.w.json │ │ │ │ ├── 10-update-name-in-same-cell-of-notebook/ │ │ │ │ │ ├── recording.w.json │ │ │ │ │ └── scoredEdits.w.json │ │ │ │ ├── 11-update-name-in-next-cell-of-notebook/ │ │ │ │ │ ├── recording.w.json │ │ │ │ │ └── scoredEdits.w.json │ │ │ │ ├── 2-helloworld-sample-remove-generic-parameter/ │ │ │ │ │ ├── recording.w.json │ │ │ │ │ └── scoredEdits.w.json │ │ │ │ ├── 5-devcontainers.github.io-part-1/ │ │ │ │ │ ├── recording.w.json │ │ │ │ │ └── scoredEdits.w.json │ │ │ │ ├── 6-vscode-remote-try-java-part-1/ │ │ │ │ │ ├── recording.w.json │ │ │ │ │ └── scoredEdits.w.json │ │ │ │ ├── 6-vscode-remote-try-java-part-2/ │ │ │ │ │ ├── recording.w.json │ │ │ │ │ └── scoredEdits.w.json │ │ │ │ ├── 8-cppIndividual-1-point.cpp/ │ │ │ │ │ ├── recording.w.json │ │ │ │ │ └── scoredEdits.w.json │ │ │ │ ├── 8-cppIndividual-2-collection-farewell/ │ │ │ │ │ ├── recording.w.json │ │ │ │ │ └── scoredEdits.w.json │ │ │ │ ├── 9-cppProject-add-header-expect-implementation/ │ │ │ │ │ ├── recording.w.json │ │ │ │ │ └── scoredEdits.w.json │ │ │ │ └── 9-cppProject-add-implementation-expect-header/ │ │ │ │ ├── recording.w.json │ │ │ │ └── scoredEdits.w.json │ │ │ ├── multiFile/ │ │ │ │ ├── multiple-questions/ │ │ │ │ │ └── package.json │ │ │ │ └── unicode-string-sequences/ │ │ │ │ └── example.js │ │ │ ├── multiFileEdit/ │ │ │ │ ├── asciiart/ │ │ │ │ │ ├── package.json │ │ │ │ │ └── src/ │ │ │ │ │ └── extension.ts │ │ │ │ ├── fibonacci/ │ │ │ │ │ ├── bar.ts │ │ │ │ │ ├── foo.ts │ │ │ │ │ ├── version1.ts │ │ │ │ │ └── version2.ts │ │ │ │ ├── filepaths/ │ │ │ │ │ ├── 1.ts │ │ │ │ │ ├── 2.ts │ │ │ │ │ └── 3.ts │ │ │ │ ├── fsprovider/ │ │ │ │ │ ├── package.json │ │ │ │ │ └── src/ │ │ │ │ │ ├── extension.ts │ │ │ │ │ └── fileSystemProvider.ts │ │ │ │ ├── issue-8098/ │ │ │ │ │ ├── debugTelemetry.ts │ │ │ │ │ └── debugUtils.ts │ │ │ │ ├── issue-8131/ │ │ │ │ │ └── extension.ts │ │ │ │ ├── issue-9130/ │ │ │ │ │ └── empty.html │ │ │ │ ├── readme-generation/ │ │ │ │ │ └── .devcontainer/ │ │ │ │ │ ├── devcontainer.json │ │ │ │ │ └── post-install.sh │ │ │ │ └── two-edits/ │ │ │ │ └── generate-command-ts.js │ │ │ ├── notebook/ │ │ │ │ ├── LICENSE │ │ │ │ ├── datacleansing.ipynb │ │ │ │ ├── dataframe.ipynb │ │ │ │ ├── edit.ipynb │ │ │ │ ├── edits/ │ │ │ │ │ ├── data_visualization.ipynb │ │ │ │ │ ├── data_visualization_2.ipynb │ │ │ │ │ ├── empty.ipynb │ │ │ │ │ ├── empty_julia.ipynb │ │ │ │ │ ├── github.ipynb │ │ │ │ │ ├── imports.ipynb │ │ │ │ │ ├── large_cell.ipynb │ │ │ │ │ ├── matplotlib_to_plotly.ipynb │ │ │ │ │ ├── multicells.ipynb │ │ │ │ │ ├── plot.ipynb │ │ │ │ │ ├── plotly_to_matplotlib.ipynb │ │ │ │ │ ├── point.ipynb │ │ │ │ │ ├── reorder.ipynb │ │ │ │ │ └── single.ipynb │ │ │ │ ├── errors.ipynb │ │ │ │ ├── fibonacci.ipynb │ │ │ │ ├── filtered-mbpp.json │ │ │ │ ├── fixing/ │ │ │ │ │ ├── fixing0.ipynb │ │ │ │ │ ├── fixing1.ipynb │ │ │ │ │ ├── fixing10.ipynb │ │ │ │ │ ├── fixing11.ipynb │ │ │ │ │ ├── fixing12.ipynb │ │ │ │ │ ├── fixing13.ipynb │ │ │ │ │ ├── fixing14.ipynb │ │ │ │ │ ├── fixing15.ipynb │ │ │ │ │ ├── fixing16.ipynb │ │ │ │ │ ├── fixing17.ipynb │ │ │ │ │ ├── fixing18.ipynb │ │ │ │ │ ├── fixing2.ipynb │ │ │ │ │ ├── fixing3.ipynb │ │ │ │ │ ├── fixing4.ipynb │ │ │ │ │ ├── fixing5.ipynb │ │ │ │ │ ├── fixing6.ipynb │ │ │ │ │ ├── fixing7.ipynb │ │ │ │ │ ├── fixing8.ipynb │ │ │ │ │ └── fixing9.ipynb │ │ │ │ ├── mbpp.ipynb │ │ │ │ ├── md.ipynb │ │ │ │ ├── model.ipynb │ │ │ │ ├── plot.ipynb │ │ │ │ ├── sales.ipynb │ │ │ │ ├── variables.ipynb │ │ │ │ └── variablesruntime.ipynb │ │ │ ├── review/ │ │ │ │ ├── bank-account-1.py │ │ │ │ ├── bank-account-2.py │ │ │ │ ├── binary-search-1.js │ │ │ │ ├── binary-search-2.js │ │ │ │ ├── nested-services-1.ts │ │ │ │ └── nested-services-2.ts │ │ │ ├── tests/ │ │ │ │ ├── another-unit-test/ │ │ │ │ │ ├── charCode.ts │ │ │ │ │ ├── strings.test.ts │ │ │ │ │ └── strings.ts │ │ │ │ ├── cs-newtest/ │ │ │ │ │ └── src/ │ │ │ │ │ └── services/ │ │ │ │ │ └── Model.cs │ │ │ │ ├── for-method-issue-3699/ │ │ │ │ │ └── foldingRanges.ts │ │ │ │ ├── generate-for-selection/ │ │ │ │ │ └── base/ │ │ │ │ │ ├── common/ │ │ │ │ │ │ └── map.ts │ │ │ │ │ └── test/ │ │ │ │ │ └── common/ │ │ │ │ │ └── map.test.ts │ │ │ │ ├── generate-jest/ │ │ │ │ │ └── some/ │ │ │ │ │ ├── app.js │ │ │ │ │ ├── sum.js │ │ │ │ │ └── sum.test.js │ │ │ │ ├── in-suite-issue-3701/ │ │ │ │ │ └── notebookFolding.test.ts │ │ │ │ ├── java-example-project/ │ │ │ │ │ ├── pom.xml │ │ │ │ │ └── src/ │ │ │ │ │ └── main/ │ │ │ │ │ └── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── example/ │ │ │ │ │ └── MyCalculator.java │ │ │ │ ├── java-example-project-with-existing-test-file/ │ │ │ │ │ ├── pom.xml │ │ │ │ │ └── src/ │ │ │ │ │ ├── main/ │ │ │ │ │ │ └── java/ │ │ │ │ │ │ └── com/ │ │ │ │ │ │ └── example/ │ │ │ │ │ │ └── MyCalculator.java │ │ │ │ │ └── test/ │ │ │ │ │ └── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── example/ │ │ │ │ │ └── MyCalculatorTest.java │ │ │ │ ├── panel/ │ │ │ │ │ └── tsq/ │ │ │ │ │ ├── foo.ts │ │ │ │ │ └── workspaceState.state.json │ │ │ │ ├── py-extra-nested/ │ │ │ │ │ ├── focus_module/ │ │ │ │ │ │ └── data_controllers/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ └── grocery.py │ │ │ │ │ └── tests/ │ │ │ │ │ └── integration/ │ │ │ │ │ └── test_other.py │ │ │ │ ├── py-newtest-4658/ │ │ │ │ │ └── ex.py │ │ │ │ ├── py-pyproject-toml/ │ │ │ │ │ ├── pyproject.toml │ │ │ │ │ ├── src/ │ │ │ │ │ │ └── mmath/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── add.py │ │ │ │ │ │ └── sub.py │ │ │ │ │ └── tests/ │ │ │ │ │ └── test_sub.py │ │ │ │ ├── py_end_test/ │ │ │ │ │ ├── src/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── ex.py │ │ │ │ │ │ └── measure.py │ │ │ │ │ └── tests/ │ │ │ │ │ └── ex_test.py │ │ │ │ ├── py_repo_root/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── temp.py │ │ │ │ ├── py_start_test/ │ │ │ │ │ ├── src/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── ex.py │ │ │ │ │ │ └── measure.py │ │ │ │ │ └── tests/ │ │ │ │ │ └── test_ex.py │ │ │ │ ├── simple-js-proj/ │ │ │ │ │ ├── package.json │ │ │ │ │ └── src/ │ │ │ │ │ └── index.js │ │ │ │ ├── simple-js-proj copy/ │ │ │ │ │ ├── package.json │ │ │ │ │ └── src/ │ │ │ │ │ └── index.js │ │ │ │ ├── simple-ts-proj/ │ │ │ │ │ ├── package.json │ │ │ │ │ ├── src/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── math.ts │ │ │ │ │ └── tsconfig.json │ │ │ │ ├── simple-ts-proj-with-test-file/ │ │ │ │ │ ├── package.json │ │ │ │ │ ├── src/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── test/ │ │ │ │ │ │ └── index.test.ts │ │ │ │ │ └── tsconfig.json │ │ │ │ ├── simple-ts-proj-with-test-file-1/ │ │ │ │ │ ├── package.json │ │ │ │ │ ├── src/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── test/ │ │ │ │ │ │ └── index.test.ts │ │ │ │ │ └── tsconfig.json │ │ │ │ ├── simple-ts-proj-with-test-file-2/ │ │ │ │ │ ├── package.json │ │ │ │ │ ├── src/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── test/ │ │ │ │ │ │ └── index.test.ts │ │ │ │ │ └── tsconfig.json │ │ │ │ ├── ts-another-test-4636/ │ │ │ │ │ └── stickyScroll.test.ts │ │ │ │ └── ts-leading-whitespace/ │ │ │ │ ├── charCode.ts │ │ │ │ ├── strings.ts │ │ │ │ └── uint.ts │ │ │ ├── tlaplus/ │ │ │ │ └── toolbox/ │ │ │ │ └── org.lamport.tla.toolbox.doc/ │ │ │ │ └── src/ │ │ │ │ └── org/ │ │ │ │ └── lamport/ │ │ │ │ └── tla/ │ │ │ │ └── toolbox/ │ │ │ │ └── doc/ │ │ │ │ └── HelpActivator.java │ │ │ ├── unknown/ │ │ │ │ └── issue-7660/ │ │ │ │ └── positionOffsetTransformer.spec.ts │ │ │ └── vscode/ │ │ │ ├── codeEditorWidget.ts │ │ │ ├── editorGroupWatermark.ts │ │ │ ├── extHost.api.impl.ts │ │ │ ├── src/ │ │ │ │ └── vs/ │ │ │ │ └── workbench/ │ │ │ │ └── api/ │ │ │ │ └── common/ │ │ │ │ └── extHostChat.ts │ │ │ └── vscode.proposed.notebookDocumentWillSave.d.ts │ │ ├── inlineChatSimulator.ts │ │ ├── inlineEdit/ │ │ │ ├── fileLoading.ts │ │ │ ├── inlineEdit.stest.ts │ │ │ ├── inlineEditScoringService.ts │ │ │ └── inlineEditTester.ts │ │ ├── language/ │ │ │ ├── lsifLanguageFeatureService.ts │ │ │ ├── simulationLanguageFeatureService.ts │ │ │ └── tsServerClient.ts │ │ ├── nesCoffeTests.ts │ │ ├── nesCoffeTestsTypes.ts │ │ ├── nesExternalTests.ts │ │ ├── nesOptionsToConfigurations.ts │ │ ├── notebookEdits.stest.ts │ │ ├── notebooks.stest.ts │ │ ├── outcomeValidators.ts │ │ ├── panelCodeMapperSimulator.ts │ │ ├── prTitleAndDescription.stest.ts │ │ ├── renameSuggestionsProvider.stest.ts │ │ ├── setupTests.stest.ts │ │ ├── shared/ │ │ │ ├── grepFilter.ts │ │ │ └── sharedTypes.ts │ │ ├── simulationTestProvider.ts │ │ ├── slash-test/ │ │ │ ├── testGen.cpp.stest.ts │ │ │ ├── testGen.csharp.stest.ts │ │ │ ├── testGen.java.stest.ts │ │ │ ├── testGen.js.stest.ts │ │ │ ├── testGen.py.stest.ts │ │ │ └── testGen.ts.stest.ts │ │ ├── stestUtil.ts │ │ ├── testInformation.ts │ │ ├── testSnapshot.ts │ │ ├── tools/ │ │ │ ├── README.md │ │ │ └── toolcall.stest.ts │ │ ├── types.ts │ │ └── workbench/ │ │ ├── components/ │ │ │ ├── amlModeToolbar.tsx │ │ │ ├── amlPicker.tsx │ │ │ ├── app.tsx │ │ │ ├── baselineJSONPicker.tsx │ │ │ ├── compareAgainstRunPicker.tsx │ │ │ ├── contextMenu.tsx │ │ │ ├── currentRunPicker.tsx │ │ │ ├── diffEditor.tsx │ │ │ ├── draggableBottomBorder.tsx │ │ │ ├── editor.tsx │ │ │ ├── errorComparison.tsx │ │ │ ├── filterUtils.tsx │ │ │ ├── localModeToolbar.tsx │ │ │ ├── monacoUtils.ts │ │ │ ├── nesExternalModeToolbar.tsx │ │ │ ├── openInVSCode.tsx │ │ │ ├── output.tsx │ │ │ ├── pickerStyle.ts │ │ │ ├── request.tsx │ │ │ ├── scorecard.tsx │ │ │ ├── scorecardByLanguage.tsx │ │ │ ├── testCaseSummary.tsx │ │ │ ├── testFilterer.tsx │ │ │ ├── testList.tsx │ │ │ ├── testRun.tsx │ │ │ ├── testView.tsx │ │ │ └── toolbar.tsx │ │ ├── initArgs.ts │ │ ├── simulationWorkbench.tsx │ │ ├── stores/ │ │ │ ├── amlResults.ts │ │ │ ├── amlSimulations.ts │ │ │ ├── baselineJSONProvider.ts │ │ │ ├── detectedTests.ts │ │ │ ├── nesExternalOptions.ts │ │ │ ├── resolvedAMLRun.ts │ │ │ ├── resolvedSimulationRun.ts │ │ │ ├── runnerOptions.ts │ │ │ ├── runnerTestStatus.ts │ │ │ ├── simulationBaseline.ts │ │ │ ├── simulationRunner.ts │ │ │ ├── simulationStorage.ts │ │ │ ├── simulationTestsProvider.ts │ │ │ ├── simulationWorkspaceState.ts │ │ │ ├── storage.ts │ │ │ ├── testRun.ts │ │ │ └── testSource.ts │ │ ├── tsconfig.json │ │ └── utils/ │ │ ├── simulationExec.ts │ │ └── utils.ts │ ├── simulationExtension/ │ │ ├── .gitignore │ │ ├── .vscodeignore │ │ └── extension.js │ ├── simulationLogger.ts │ ├── simulationMain.ts │ ├── simulationTests.ts │ ├── taskRunner.ts │ ├── testExecutionInExtension.ts │ ├── testExecutor.ts │ ├── testVisualizationRunner.ts │ ├── testVisualizationRunnerSTest.ts │ ├── testVisualizationRunnerSTestRunner.ts │ └── util.ts ├── tsconfig.base.json ├── tsconfig.json ├── tsconfig.worker.json ├── tsfmt.json └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .agents/skills/launch/SKILL.md ================================================ --- name: launch description: "Launch and automate VS Code Insiders with the Copilot Chat extension using agent-browser via Chrome DevTools Protocol. Use when you need to interact with the VS Code UI, automate the chat panel, test the extension UI, or take screenshots. Triggers include 'automate VS Code', 'interact with chat', 'test the UI', 'take a screenshot', 'launch with debugging'." metadata: allowed-tools: Bash(agent-browser:*), Bash(npx agent-browser:*) --- # VS Code Extension Automation Automate VS Code Insiders with the Copilot Chat extension using agent-browser. VS Code is built on Electron/Chromium and exposes a Chrome DevTools Protocol (CDP) port that agent-browser can connect to, enabling the same snapshot-interact workflow used for web pages. ## Prerequisites - **`agent-browser` must be installed.** It's available in the project's devDependencies — run `npm install`. Use `npx agent-browser` if it's not on your PATH, or install globally with `npm install -g agent-browser`. - **`code-insiders` is required.** This extension uses 58 proposed VS Code APIs and targets `vscode ^1.110.0-20260223`. VS Code Stable will **not** activate it — you must use VS Code Insiders. - **The extension must be compiled first.** Use `npm run compile` for a one-shot build, or `npm run watch` for iterative development. - **CSS selectors are internal implementation details.** Selectors like `.interactive-input-part`, `.monaco-editor`, and `.view-line` are VS Code internals that may change across versions. If automation breaks after a VS Code update, re-snapshot and check for selector changes. ## Core Workflow 1. **Build** the extension 2. **Launch** VS Code Insiders with the extension and remote debugging enabled 3. **Connect** agent-browser to the CDP port 4. **Snapshot** to discover interactive elements 5. **Interact** using element refs 6. **Re-snapshot** after navigation or state changes > **📸 Take screenshots for a paper trail.** Use `agent-browser screenshot ` at key moments — after launch, before/after interactions, and when something goes wrong. Screenshots provide visual proof of what the UI looked like and are invaluable for debugging failures or documenting what was accomplished. > > Save screenshots inside `.vscode-ext-debug/screenshots/` (gitignored) using a timestamped subfolder so each run is isolated and nothing gets overwritten: > > ```bash > # Create a timestamped folder for this run's screenshots > SCREENSHOT_DIR=".vscode-ext-debug/screenshots/$(date +%Y-%m-%dT%H-%M-%S)" > mkdir -p "$SCREENSHOT_DIR" > > # Windows (PowerShell): > # $screenshotDir = ".vscode-ext-debug\screenshots\$(Get-Date -Format 'yyyy-MM-ddTHH-mm-ss')" > # New-Item -ItemType Directory -Force -Path $screenshotDir > > # Save a screenshot (path is a positional argument — use ./ or absolute paths) > # Bare filenames without ./ may be misinterpreted as CSS selectors > agent-browser screenshot "$SCREENSHOT_DIR/after-launch.png" > ``` ```bash # Build and launch with the extension npm run compile # Use a PERSISTENT user-data-dir so auth state is preserved across sessions. # .vscode-ext-debug is relative to the project root — works in worktrees and is gitignored. code-insiders --extensionDevelopmentPath="$PWD" --remote-debugging-port=9223 --user-data-dir="$PWD/.vscode-ext-debug" # On Windows (PowerShell): # code-insiders --extensionDevelopmentPath="$PWD" --remote-debugging-port=9223 --user-data-dir="$PWD\.vscode-ext-debug" # Wait for VS Code to start, retry until connected for i in 1 2 3 4 5; do agent-browser connect 9223 2>/dev/null && break || sleep 3; done # Verify you're connected to the right target (not about:blank) # If `tab` shows the wrong target, run `agent-browser close` and reconnect agent-browser tab agent-browser snapshot -i ``` ## Connecting ```bash # Connect to a specific port agent-browser connect 9223 # Or use --cdp on each command agent-browser --cdp 9223 snapshot -i # Auto-discover a running Chromium-based app agent-browser --auto-connect snapshot -i ``` After `connect`, all subsequent commands target the connected app without needing `--cdp`. ## Tab Management VS Code uses multiple webviews internally. Use tab commands to list and switch between them: ```bash # List all available targets (windows, webviews, etc.) agent-browser tab # Switch to a specific tab by index agent-browser tab 2 # Switch by URL pattern agent-browser tab --url "*settings*" ``` ## Launching VS Code Extensions for Debugging To debug a VS Code extension via agent-browser, launch VS Code Insiders with `--extensionDevelopmentPath` pointing to your extension source and `--remote-debugging-port` for CDP. Use `--user-data-dir` to avoid conflicting with an already-running VS Code instance. ```bash # Build the extension first (from the repo root) npm run compile # Launch VS Code Insiders with the extension and CDP # IMPORTANT: Use a persistent directory (not /tmp) so auth state is preserved. # .vscode-ext-debug is relative to the project root — works in worktrees and is gitignored. code-insiders \ --extensionDevelopmentPath="$PWD" \ --remote-debugging-port=9223 \ --user-data-dir="$PWD/.vscode-ext-debug" # Wait for VS Code to start, retry until connected for i in 1 2 3 4 5; do agent-browser connect 9223 2>/dev/null && break || sleep 3; done # Verify you're connected to the right target (not about:blank) # If `tab` shows the wrong target, run `agent-browser close` and reconnect agent-browser tab agent-browser snapshot -i ``` **Key flags:** - `--extensionDevelopmentPath=` — loads your extension from source (must be compiled first). Use `$PWD` when running from the repo root. - `--remote-debugging-port=9223` — enables CDP (use 9223 to avoid conflicts with other apps on 9222) - `--user-data-dir=` — uses a separate profile so it starts a new process instead of sending to an existing VS Code instance. **Always use a persistent path** (e.g., `$PWD/.vscode-ext-debug`) rather than `/tmp/...` so authentication, settings, and extension state survive across sessions. **Without `--user-data-dir`**, VS Code detects the running instance, forwards the args to it, and exits immediately — you'll see "Sent env to running instance. Terminating..." and CDP never starts. > **⚠️ Authentication is required.** The Copilot Chat extension needs an authenticated GitHub session to function. Using a temp directory (e.g., `/tmp/...`) creates a fresh profile with no auth — the agent will hit a "Sign in to use Copilot" wall and model resolution will fail with "Language model unavailable." > > **Always use a persistent `--user-data-dir`** like `$PWD/.vscode-ext-debug`. On first use, launch once and sign in manually. Subsequent launches will reuse the auth session. ## Restarting After Code Changes **After making changes to the extension source code, you must restart VS Code to pick up the new build.** The extension host loads the compiled bundle at startup — changes are not hot-reloaded. ### Restart Workflow 1. **Recompile** the extension 2. **Kill** the running VS Code instance (the one using your debug user-data-dir) 3. **Relaunch** VS Code with the same flags ```bash # 1. Recompile npm run compile # 2. Kill the VS Code instance tied to this project's debug profile, then relaunch # macOS / Linux: kill $(ps ax -ww -o pid,command | grep "$PWD/.vscode-ext-debug" | grep -v grep | awk '{print $1}' | head -1) # Windows (PowerShell): # Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like "*$PWD\.vscode-ext-debug*" } | ForEach-Object { Stop-Process -Id $_.ProcessId } # 3. Relaunch code-insiders \ --extensionDevelopmentPath="$PWD" \ --remote-debugging-port=9223 \ --user-data-dir="$PWD/.vscode-ext-debug" # 4. Reconnect agent-browser for i in 1 2 3 4 5; do agent-browser connect 9223 2>/dev/null && break || sleep 3; done agent-browser snapshot -i ``` > **Tip:** If you're iterating frequently, run `npm run watch` in a separate terminal so compilation happens automatically. You still need to kill and relaunch VS Code to load the new bundle. ## Interacting with Monaco Editor (Chat Input, Code Editors) VS Code uses Monaco Editor for all text inputs including the Copilot Chat input. Monaco editors appear as textboxes in the accessibility snapshot but require specific agent-browser commands to interact with. ### What Works #### `type @ref` — The Best Approach The `type` command with a snapshot ref handles focus and input in one step: ```bash # Snapshot to find the chat input ref agent-browser snapshot -i # Look for: textbox "The editor is not accessible..." [ref=e51] # Type directly using the ref — handles focus automatically agent-browser type @e51 "Hello from George!" # Send the message agent-browser press Enter # Wait for the response to complete before re-snapshotting. # Poll until the "Stop generating" button disappears: for i in $(seq 1 30); do agent-browser snapshot -i 2>/dev/null | grep -q "Stop generating" || break sleep 1 done agent-browser snapshot -i ``` This is the simplest and most reliable method. It works for both the main editor chat input and the sidebar chat panel. > **Tip:** If `type @ref` silently drops text (the editor stays empty), the ref may be stale or the editor not yet ready. Re-snapshot to get a fresh ref and try again. You can verify text was entered using the snippet in "Verifying Text in Monaco" below. #### `keyboard type` / `keyboard inserttext` — After Focus If focus is already on a Monaco editor, `keyboard type` and `keyboard inserttext` both work: > **⚠️ Warning:** `keyboard type` can hang indefinitely in some focus states (e.g., after JS mouse events). If it doesn't return within a few seconds, interrupt it and fall back to `press` for individual keystrokes. ```bash # Focus first (via type @ref, or JS mouse events, or a prior interaction) agent-browser type @e51 "" # Then keyboard type works for subsequent input agent-browser keyboard type "More text here" agent-browser keyboard inserttext "And this too" ``` #### `press` — Individual Keystrokes Always works when focus is on a Monaco editor. Useful for special keys, keyboard shortcuts, and as a universal fallback for typing text character by character: ```bash # Type text character by character (works on all builds) agent-browser press H agent-browser press e agent-browser press l agent-browser press l agent-browser press o agent-browser press Space # Use "Space" for spaces # Select all # macOS: agent-browser press Meta+a # Linux / Windows: agent-browser press Control+a agent-browser press Backspace # Delete selection agent-browser press Enter # Send message / new line # Send to new chat # macOS: agent-browser press Meta+Shift+Enter # Linux / Windows: agent-browser press Control+Shift+Enter ``` ### What Does NOT Work | Method | Result | Reason | |--------|--------|--------| | `click @ref` | "Element blocked by another element" | Monaco overlays a transparent div over the textarea | | `fill @ref "text"` | "Element not found or not visible" | The textbox is not a standard fillable input | | `document.execCommand("insertText")` via `eval` | No effect | Monaco intercepts and discards execCommand | | Setting `textarea.value` + dispatching `input` event via `eval` | No effect | Monaco doesn't read from the textarea's value property | ### Fallback: Focus via JavaScript Mouse Events If `type @ref` doesn't work (e.g., ref is stale), you can focus the editor via JavaScript: ```bash agent-browser eval ' (() => { const inputPart = document.querySelector(".interactive-input-part"); const editor = inputPart.querySelector(".monaco-editor"); const rect = editor.getBoundingClientRect(); const x = rect.x + rect.width / 2; const y = rect.y + rect.height / 2; editor.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, clientX: x, clientY: y })); editor.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, clientX: x, clientY: y })); editor.dispatchEvent(new MouseEvent("click", { bubbles: true, clientX: x, clientY: y })); return "activeElement: " + document.activeElement?.className; })()' # After JS focus, keyboard type and press work agent-browser keyboard type "Text after JS focus" ``` After JS mouse events, `document.activeElement` becomes a `DIV` with class `native-edit-context` — this is VS Code's native text editing surface. ### Verifying Text in Monaco Monaco renders text in `.view-line` elements, not the textarea: ```bash agent-browser eval ' (() => { const inputPart = document.querySelector(".interactive-input-part"); return Array.from(inputPart.querySelectorAll(".view-line")).map(vl => vl.textContent).join("|"); })()' ``` ### Clearing Monaco Input ```bash # macOS: agent-browser press Meta+a # Linux / Windows: agent-browser press Control+a agent-browser press Backspace ``` ## Troubleshooting ### "Connection refused" or "Cannot connect" - Make sure VS Code Insiders was launched with `--remote-debugging-port=9223` - If VS Code was already running, quit and relaunch with the flag - Check that the port isn't in use by another process: - macOS / Linux: `lsof -i :9223` - Windows: `netstat -ano | findstr 9223` ### Elements not appearing in snapshot - VS Code uses multiple webviews. Use `agent-browser tab` to list targets and switch to the right one - Use `agent-browser snapshot -i -C` to include cursor-interactive elements (divs with onclick handlers) ### Cannot type in Monaco inputs - Standard `click` and `fill` don't work on Monaco editors — see the "Interacting with Monaco Editor" section above for the full compatibility matrix - `type @ref` is the best approach; individual `press` commands work everywhere; `keyboard type` and `keyboard inserttext` work after focus is established ### Screenshots fail with "Permission denied" (macOS) If `agent-browser screenshot` returns "Permission denied", your terminal needs Screen Recording permission. Grant it in **System Settings → Privacy & Security → Screen Recording**. As a fallback, use the `eval` verification snippet to confirm text was entered — this doesn't require screen permissions. ## Cleanup **Always kill the debug VS Code instance when you're done.** Leaving it running wastes resources and holds the CDP port. ```bash # Disconnect agent-browser agent-browser close # Kill the debug VS Code instance # macOS / Linux: kill $(ps ax -ww -o pid,command | grep "$PWD/.vscode-ext-debug" | grep -v grep | awk '{print $1}' | head -1) # Windows (PowerShell): # Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like "*$PWD\.vscode-ext-debug*" } | ForEach-Object { Stop-Process -Id $_.ProcessId } ``` ================================================ FILE: .claude/agents/anthropic-sdk-upgrader.md ================================================ --- name: anthropic-sdk-upgrader description: "Use this agent when the user needs to upgrade Anthropic SDK packages. This includes: upgrading @anthropic-ai/sdk or @anthropic-ai/claude-agent-sdk to newer versions, migrating between SDK versions, resolving SDK-related dependency conflicts, updating SDK types and interfaces, or asking about SDK upgrade procedures. Examples: 'Upgrade the Anthropic SDK to the latest version', 'Help me migrate to the latest claude-agent-sdk', 'What's the process for upgrading Anthropic packages?'" model: opus --- You are an expert at upgrading Anthropic SDK packages in the vscode-copilot-chat project. ## Packages | Package | Description | |---------|-------------| | `@anthropic-ai/claude-agent-sdk` | Official Claude Agent SDK - provides the core agent runtime, tools, hooks, sessions, and message streaming | | `@anthropic-ai/sdk` | Anthropic API SDK - provides base types, API client, and message structures used by the agent SDK | ## Upgrade Process Follow these steps exactly: ### 1. Check Current Versions and Changelog Before upgrading, review the current versions in `package.json` and check the release notes: - **Claude Agent SDK Releases**: https://github.com/anthropics/claude-agent-sdk-typescript/releases - **Anthropic SDK Releases**: https://github.com/anthropics/anthropic-sdk-typescript/releases ### 2. Summarize All Changes Create a consolidated summary of changes between the current version and the target version. Group changes by category, not by individual version: **Summary Format:** ```markdown ### `@anthropic-ai/package-name` (oldVersion → newVersion) #### Features - **Category:** Description of new feature or capability #### Bug Fixes - Description of what was fixed #### Breaking Changes - **Old API → New API**: Description of what changed and how to migrate ``` **How to Create the Summary:** 1. **Read the GitHub Release Notes**: Go through each release between your versions 2. **Consolidate by Category**: Group all features together, all bug fixes together, etc. 3. **Identify Breaking Changes**: Look for: - Removed or renamed exports - Changed function signatures - Modified type definitions - Deprecated APIs that have been removed 4. **Document Migration Steps**: For breaking changes, include the old and new patterns 5. **Check Peer Dependencies**: Note if the new version requires different peer dependencies ### 3. List Important Changes Categorize changes by impact level: **Critical (Must Address Before Merge):** - Breaking API changes that will cause compilation errors - Removed types or functions currently in use - Changed behavior of core functionality (sessions, streaming, tools) **Important (Should Address):** - Deprecated APIs that should be migrated - New recommended patterns replacing old ones - Performance improvements that require code changes **Nice to Have (Can Address Later):** - New optional features - Additional type exports - Enhanced error messages ### 4. Update Package Versions ```bash # Update to latest npm install @anthropic-ai/claude-agent-sdk @anthropic-ai/sdk ``` ### 5. Detect API Surface Changes After updating, diff the old and new type definitions to detect API changes that may not cause compilation errors but are important to know about (new parameters, new functions, deprecated APIs, etc.). **Steps:** 1. **Snapshot before upgrading**: Before running `npm install` in step 4, copy the current type definitions to a temp directory: ```bash mkdir -p /tmp/anthropic-sdk-old cp -r node_modules/@anthropic-ai/sdk/*.d.ts node_modules/@anthropic-ai/sdk/resources/*.d.ts /tmp/anthropic-sdk-old/ 2>/dev/null cp -r node_modules/@anthropic-ai/claude-agent-sdk/*.d.ts /tmp/anthropic-sdk-old/ 2>/dev/null ``` > **Important**: This snapshot must be taken *before* step 4's `npm install`. 2. **Diff the type definitions**: After `npm install`, compare the old and new `.d.ts` files: ```bash # Diff the Anthropic SDK types for f in node_modules/@anthropic-ai/sdk/*.d.ts node_modules/@anthropic-ai/sdk/resources/*.d.ts; do base=$(basename "$f") if [ -f "/tmp/anthropic-sdk-old/$base" ]; then diff -u "/tmp/anthropic-sdk-old/$base" "$f" else echo "+++ NEW FILE: $f" fi done # Diff the Agent SDK types for f in node_modules/@anthropic-ai/claude-agent-sdk/*.d.ts; do base=$(basename "$f") if [ -f "/tmp/anthropic-sdk-old/$base" ]; then diff -u "/tmp/anthropic-sdk-old/$base" "$f" else echo "+++ NEW FILE: $f" fi done ``` 3. **Analyze the diff and produce a report** with the following categories: **New Exports** — Functions, classes, types, or constants that were added: - New exported functions or methods - New type/interface definitions - New enum values **New Parameters** — Optional or required parameters added to existing functions: - New optional fields on existing option/config types - New required parameters (these are breaking changes — flag them as critical) - New overloads of existing functions **Changed Signatures** — Modifications to existing function/method signatures: - Parameter type changes (e.g., `string` → `string | string[]`) - Return type changes - Generic type parameter changes **Removed or Renamed** — Items that were removed or renamed: - Removed exports (breaking — flag as critical) - Renamed types/functions (breaking — flag as critical) - Removed fields from interfaces **Deprecations** — Items newly marked as `@deprecated`: - Functions or types with new `@deprecated` JSDoc tags 4. **Cross-reference with our usage**: For each change found, check whether the codebase currently uses the affected API: ```bash # Example: if `createSession` gained a new parameter, check our usage grep -rn "createSession" src/extension/agents/claude/ ``` Flag changes that affect APIs we actively use as higher priority. 5. **Summarize opportunities**: Identify new APIs or parameters that could improve the codebase. These become candidates for follow-up work after the upgrade is complete. 6. **Clean up**: ```bash rm -rf /tmp/anthropic-sdk-old ``` ### 6. Fix Compilation Errors After updating, check for compilation errors: ```bash npm run compile ``` Address any type errors in the following key files: - `src/extension/agents/claude/node/claudeCodeAgent.ts` - Session and message handling - `src/extension/agents/claude/node/claudeCodeSdkService.ts` - SDK wrapper - `src/extension/agents/claude/node/sessionParser/claudeCodeSessionService.ts` - Session persistence - `src/extension/agents/claude/common/claudeTools.ts` - Tool type definitions - `src/extension/agents/claude/node/hooks/*.ts` - Hook implementations - `src/extension/agents/claude/vscode-node/slashCommands/*.ts` - Slash command handlers - `src/extension/agents/claude/node/toolPermissionHandlers/*.ts` - Permission handlers ### 7. Run Tests After upgrading, run the Claude-related unit tests to verify nothing is broken: ```bash # Run all Claude agent tests npm run test:unit -- --testPathPattern="agents/claude" ``` Fix any test failures before proceeding. Common test files to check: - `src/extension/agents/claude/node/test/claudeCodeAgent.spec.ts` - `src/extension/agents/claude/node/test/claudeCodeSessionService.spec.ts` - `src/extension/agents/claude/node/sessionParser/test/*.spec.ts` ### 8. Update Documentation If needed, update documentation in the codebase: 1. Update `src/extension/agents/claude/AGENTS.md` if any architectural changes occurred 2. Update type definitions in `common/claudeTools.ts` if tools changed 3. Document any new features or capabilities added 4. Update the "Official Claude Agent SDK Documentation" links if URLs changed ### 9. Commit with a Detailed Message Create a commit message that documents the upgrade clearly. Include: 1. **Package version changes** - Both old and new versions 2. **Features** - Notable new capabilities added 3. **Bug fixes** - Important fixes included 4. **Breaking changes** - What changed and how it was addressed in the code **Example commit message:** ``` Update Anthropic SDK packages ### `@anthropic-ai/sdk` (0.71.2 → 0.72.1) #### Features - Structured Outputs support in Messages API - MCP SDK helper functions #### Breaking Changes - `output_format` → `output_config` parameter migration ### `@anthropic-ai/claude-agent-sdk` (0.2.5 → 0.2.31) #### Features - **Query interface:** Added `close()` method, `reconnectMcpServer()`, `toggleMcpServer()` methods - **Sessions:** Added `listSessions()` function for discovering resumable sessions - **MCP:** Added `config`, `scope`, `tools` fields and `disabled` status to `McpServerStatus` #### Bug Fixes - Fixed `mcpServerStatus()` to include tools from SDK and dynamically-added MCP servers - Fixed PermissionRequest hooks in SDK mode #### Breaking Changes - `KillShellInput` → `TaskStopInput`: Updated type mapping in claudeTools.ts ``` ## Troubleshooting Common Issues **Type Errors After Upgrade:** - Check if types were renamed (common: `Message` → `ContentBlock`, etc.) - Look for removed type exports that need new imports - Verify generic type parameters haven't changed **Session Loading Failures:** - Session file format may have changed between major versions - Check `ClaudeCodeSessionService` for compatibility issues - May need to clear old session files during major upgrades **Hook Registration Failures:** - Hook event names may have changed - Check `HookEvent` type for valid event strings - Verify hook callback signatures match new SDK expectations **Tool Execution Errors:** - Tool input schemas may have changed - Check tool result handling for new error types - Verify tool confirmation flow hasn't changed ================================================ FILE: .devcontainer/devcontainer-lock.json ================================================ { "features": { "ghcr.io/devcontainers/features/azure-cli:1": { "version": "1.2.7", "resolved": "ghcr.io/devcontainers/features/azure-cli@sha256:91ffef641dbe5045b9982921487d743f7a3047cc05efd9226345833f446c8bce", "integrity": "sha256:91ffef641dbe5045b9982921487d743f7a3047cc05efd9226345833f446c8bce" }, "ghcr.io/devcontainers/features/desktop-lite:1": { "version": "1.2.8", "resolved": "ghcr.io/devcontainers/features/desktop-lite@sha256:14ac23fd59afab939e6562ba6a1f42a659a805e4c574a1be23b06f28eb3b0b71", "integrity": "sha256:14ac23fd59afab939e6562ba6a1f42a659a805e4c574a1be23b06f28eb3b0b71" }, "ghcr.io/devcontainers/features/docker-in-docker:2.16.1": { "version": "2.16.1", "resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:ce078b7bf7d9ef3bcb9813b32103795d8d72172446890b64772cbe1dec6baafd", "integrity": "sha256:ce078b7bf7d9ef3bcb9813b32103795d8d72172446890b64772cbe1dec6baafd" }, "ghcr.io/devcontainers/features/dotnet:2": { "version": "2.2.2", "resolved": "ghcr.io/devcontainers/features/dotnet@sha256:06f4ef2c23792da4832a74da195d478d8f64316c45c7624a0367d6bd5c3fc500", "integrity": "sha256:06f4ef2c23792da4832a74da195d478d8f64316c45c7624a0367d6bd5c3fc500" }, "ghcr.io/devcontainers/features/git-lfs:1": { "version": "1.2.5", "resolved": "ghcr.io/devcontainers/features/git-lfs@sha256:71c2b371cf12ab7fcec47cf17369c6f59156100dad9abf9e4c593049d789de72", "integrity": "sha256:71c2b371cf12ab7fcec47cf17369c6f59156100dad9abf9e4c593049d789de72" }, "ghcr.io/devcontainers/features/python:1": { "version": "1.7.1", "resolved": "ghcr.io/devcontainers/features/python@sha256:cf9b6d879790a594b459845b207c5e1762a0c8f954bb8033ff396e497f9c301b", "integrity": "sha256:cf9b6d879790a594b459845b207c5e1762a0c8f954bb8033ff396e497f9c301b" } } } ================================================ FILE: .devcontainer/devcontainer.json ================================================ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node { "name": "Node.js & TypeScript", "image": "mcr.microsoft.com/devcontainers/typescript-node:4-24", "features": { "ghcr.io/devcontainers/features/docker-in-docker:2.16.1": { "moby": false }, "ghcr.io/devcontainers/features/azure-cli:1": {}, "ghcr.io/devcontainers/features/python:1": {}, "ghcr.io/devcontainers/features/dotnet:2": {}, "ghcr.io/devcontainers/features/desktop-lite:1": {}, "ghcr.io/devcontainers/features/git-lfs:1": {} }, "containerEnv": { "DEBIAN_FRONTEND": "noninteractive" }, "customizations": { "vscode": { "extensions": [ "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "connor4312.esbuild-problem-matchers", "GitHub.copilot@prerelease", "ms-vscode.extension-test-runner" ] } }, "hostRequirements": { "cpus": 4 }, "containerUser": "node", "onCreateCommand": { "initGitLfs": "git lfs install --force", "npmInstall": "npm install || true" } } ================================================ FILE: .esbuild.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as watcher from '@parcel/watcher'; import * as esbuild from 'esbuild'; import * as fs from 'fs'; import { copyFile, mkdir, readdir, rename } from 'fs/promises'; import { glob } from 'glob'; import * as path from 'path'; const REPO_ROOT = import.meta.dirname; const isWatch = process.argv.includes('--watch'); const isDev = process.argv.includes('--dev'); const isPreRelease = process.argv.includes('--prerelease'); const generateSourceMaps = process.argv.includes('--sourcemaps'); const sourceMapOutDir = './dist-sourcemaps'; const baseBuildOptions = { bundle: true, logLevel: 'info', minify: !isDev, outdir: './dist', // In dev mode, use linked source maps for debugging. // With --sourcemaps flag, generate external source maps (no sourceMappingURL comment in output). sourcemap: isDev ? 'linked' : (generateSourceMaps ? 'external' : false), sourcesContent: false, treeShaking: true } satisfies esbuild.BuildOptions; const baseNodeBuildOptions = { ...baseBuildOptions, external: [ './package.json', './.vscode-test.mjs', 'playwright', 'keytar', '@azure/functions-core', 'applicationinsights-native-metrics', '@opentelemetry/instrumentation', '@azure/opentelemetry-instrumentation-azure-sdk', 'electron', // this is for simulation workbench, 'sqlite3', 'node-pty', // Required by @github/copilot '@github/copilot', ...(isDev ? [] : ['dotenv', 'source-map-support']) ], platform: 'node', mainFields: ["module", "main"], // needed for jsonc-parser, define: { 'process.env.APPLICATIONINSIGHTS_CONFIGURATION_CONTENT': JSON.stringify(JSON.stringify({ proxyHttpUrl: "", proxyHttpsUrl: "" })) }, } satisfies esbuild.BuildOptions; const webviewBuildOptions = { ...baseBuildOptions, platform: 'browser', target: 'es2024', // Electron 34 -> Chrome 132 -> ES2024 entryPoints: [ { in: 'src/extension/completions-core/vscode-node/extension/src/copilotPanel/webView/suggestionsPanelWebview.ts', out: 'suggestionsPanelWebview' }, ], } satisfies esbuild.BuildOptions; const nodeExtHostTestGlobs = [ 'src/**/vscode/**/*.test.{ts,tsx}', 'src/**/vscode-node/**/*.test.{ts,tsx}', // deprecated 'src/extension/**/*.test.{ts,tsx}' ]; const testBundlePlugin: esbuild.Plugin = { name: 'testBundlePlugin', setup(build) { build.onResolve({ filter: /[\/\\]test-extension\.ts$/ }, args => { if (args.kind !== 'entry-point') { return; } return { path: path.resolve(args.path) }; }); build.onLoad({ filter: /[\/\\]test-extension\.ts$/ }, async args => { let files = await glob(nodeExtHostTestGlobs, { cwd: REPO_ROOT, posix: true, ignore: ['src/extension/completions-core/**/*'] }); files = files.map(f => path.posix.relative('src', f)); if (files.length === 0) { throw new Error('No extension tests found'); } return { contents: files .map(f => `require('./${f}');`) .join(''), watchDirs: files.map(path.dirname), watchFiles: files, }; }); } }; const nodeExtHostSanityTestGlobs = [ 'src/**/vscode-node/**/*.sanity-test.{ts,tsx}', ]; const sanityTestBundlePlugin: esbuild.Plugin = { name: 'sanityTestBundlePlugin', setup(build) { build.onResolve({ filter: /[\/\\]sanity-test-extension\.ts$/ }, args => { if (args.kind !== 'entry-point') { return; } return { path: path.resolve(args.path) }; }); build.onLoad({ filter: /[\/\\]sanity-test-extension\.ts$/ }, async args => { let files = await glob(nodeExtHostSanityTestGlobs, { cwd: REPO_ROOT, posix: true, ignore: ['src/extension/completions-core/**/*'] }); files = files.map(f => path.posix.relative('src', f)); if (files.length === 0) { throw new Error('No extension tests found'); } return { contents: files .map(f => `require('./${f}');`) .join(''), watchDirs: files.map(path.dirname), watchFiles: files, }; }); } }; const importMetaPlugin: esbuild.Plugin = { name: 'claudeAgentSdkImportMetaPlugin', setup(build) { // Handle import.meta.url in @anthropic-ai/claude-agent-sdk package build.onLoad({ filter: /node_modules[\/\\]@anthropic-ai[\/\\]claude-agent-sdk[\/\\].*\.mjs$/ }, async (args) => { const contents = await fs.promises.readFile(args.path, 'utf8'); return { contents: contents.replace( /import\.meta\.url/g, 'require("url").pathToFileURL(__filename).href' ), loader: 'js' }; }); } }; const shimVsCodeTypesPlugin: esbuild.Plugin = { name: 'shimVsCodeTypesPlugin', setup(build) { // Create a virtual module that will try to require vscode at runtime build.onResolve({ filter: /^vscode$/ }, args => { return { path: 'vscode-dynamic', namespace: 'vscode-fallback' }; }); build.onLoad({ filter: /^vscode-dynamic$/, namespace: 'vscode-fallback' }, () => { return { contents: ` let vscode; // See test/simulationExtension/extension.js for where and why this is created. if (typeof COPILOT_SIMULATION_VSCODE !== 'undefined') { vscode = COPILOT_SIMULATION_VSCODE; } else { try { vscode = eval('require(' + JSON.stringify('vscode') + ')'); } catch (e) { vscode = require('./src/util/common/test/shims/vscodeTypesShim.ts'); } } module.exports = vscode; `, resolveDir: REPO_ROOT }; }); } }; const nodeExtHostBuildOptions = { ...baseNodeBuildOptions, entryPoints: [ { in: './src/extension/extension/vscode-node/extension.ts', out: 'extension' }, { in: './src/platform/parser/node/parserWorker.ts', out: 'worker2' }, { in: './src/platform/tokenizer/node/tikTokenizerWorker.ts', out: 'tikTokenizerWorker' }, { in: './src/platform/diff/node/diffWorkerMain.ts', out: 'diffWorker' }, { in: './src/platform/tfidf/node/tfidfWorker.ts', out: 'tfidfWorker' }, { in: './src/extension/onboardDebug/node/copilotDebugWorker/index.ts', out: 'copilotDebugCommand' }, { in: './src/extension/chatSessions/vscode-node/copilotCLIShim.ts', out: 'copilotCLIShim' }, { in: './src/test-extension.ts', out: 'test-extension' }, { in: './src/sanity-test-extension.ts', out: 'sanity-test-extension' }, ], loader: { '.ps1': 'text' }, plugins: [testBundlePlugin, sanityTestBundlePlugin, importMetaPlugin], external: [ ...baseNodeBuildOptions.external, 'vscode' ] } satisfies esbuild.BuildOptions; const webExtHostBuildOptions = { ...baseBuildOptions, platform: 'browser', entryPoints: [ { in: './src/extension/extension/vscode-worker/extension.ts', out: 'web' }, ], format: 'cjs', // Necessary to export activate function from bundle for extension external: [ 'vscode', 'http', ] } satisfies esbuild.BuildOptions; const nodeExtHostSimulationTestOptions = { ...nodeExtHostBuildOptions, outdir: '.vscode/extensions/test-extension/dist', entryPoints: [ { in: '.vscode/extensions/test-extension/main.ts', out: './simulation-extension' } ] } satisfies esbuild.BuildOptions; const nodeSimulationBuildOptions = { ...baseNodeBuildOptions, entryPoints: [ { in: './test/simulationMain.ts', out: 'simulationMain' }, ], plugins: [testBundlePlugin, shimVsCodeTypesPlugin], external: [ ...baseNodeBuildOptions.external, ] } satisfies esbuild.BuildOptions; const nodeSimulationWorkbenchUIBuildOptions = { ...baseNodeBuildOptions, platform: 'browser', // @ulugbekna: important to target 'browser' for correct bundling using 'window' mainFields: ["browser", "module", "main"], entryPoints: [ { in: './test/simulation/workbench/simulationWorkbench.tsx', out: 'simulationWorkbench' }, ], alias: { 'vscode': './src/util/common/test/shims/vscodeTypesShim.ts' }, external: [ ...baseNodeBuildOptions.external, '../../node_modules/monaco-editor/*', // @ulugbekna: libs provided by node that need to be specified manually because of 'platform' is set to 'browser' 'fs', 'path', 'readline', 'child_process', 'http', 'assert', ], } satisfies esbuild.BuildOptions; async function typeScriptServerPluginPackageJsonInstall(): Promise { await mkdir('./node_modules/@vscode/copilot-typescript-server-plugin', { recursive: true }); const source = path.join(import.meta.dirname, './src/extension/typescriptContext/serverPlugin/package.json'); const destination = path.join(import.meta.dirname, './node_modules/@vscode/copilot-typescript-server-plugin/package.json'); try { await copyFile(source, destination); } catch (error) { console.error('Error copying package.json:', error); } } const typeScriptServerPluginBuildOptions = { bundle: true, format: 'cjs', // keepNames: true, logLevel: 'info', minify: !isDev, outdir: './node_modules/@vscode/copilot-typescript-server-plugin/dist', platform: 'node', sourcemap: isDev ? 'linked' : false, sourcesContent: false, treeShaking: true, external: [ "typescript", "typescript/lib/tsserverlibrary" ], entryPoints: [ { in: './src/extension/typescriptContext/serverPlugin/src/node/main.ts', out: 'main' }, ] } satisfies esbuild.BuildOptions; /** * Moves all .map files from the output directories to a separate source maps directory. * This keeps source maps out of the packaged extension while making them available for upload. */ async function moveSourceMapsToSeparateDir(): Promise { if (!generateSourceMaps) { return; } const outputDirs = [ './dist', './node_modules/@vscode/copilot-typescript-server-plugin/dist', ]; await mkdir(sourceMapOutDir, { recursive: true }); for (const dir of outputDirs) { try { const files = await readdir(dir); for (const file of files) { if (file.endsWith('.map')) { const sourcePath = path.join(dir, file); // Prefix with directory name to avoid collisions const prefix = dir === './dist' ? '' : 'ts-plugin-'; const destPath = path.join(sourceMapOutDir, prefix + file); await rename(sourcePath, destPath); console.log(`Moved source map: ${sourcePath} -> ${destPath}`); } } } catch (error) { // Directory might not exist in some build configurations console.warn(`Could not process directory ${dir}:`, error); } } } async function main() { if (!isDev) { applyPackageJsonPatch(isPreRelease); } await typeScriptServerPluginPackageJsonInstall(); if (isWatch) { const contexts: esbuild.BuildContext[] = []; const nodeExtHostContext = await esbuild.context(nodeExtHostBuildOptions); contexts.push(nodeExtHostContext); const webExtHostContext = await esbuild.context(webExtHostBuildOptions); contexts.push(webExtHostContext); const nodeSimulationContext = await esbuild.context(nodeSimulationBuildOptions); contexts.push(nodeSimulationContext); const nodeSimulationWorkbenchUIContext = await esbuild.context(nodeSimulationWorkbenchUIBuildOptions); contexts.push(nodeSimulationWorkbenchUIContext); const nodeExtHostSimulationContext = await esbuild.context(nodeExtHostSimulationTestOptions); contexts.push(nodeExtHostSimulationContext); const typeScriptServerPluginContext = await esbuild.context(typeScriptServerPluginBuildOptions); contexts.push(typeScriptServerPluginContext); let debounce: NodeJS.Timeout | undefined; const rebuild = async () => { if (debounce) { clearTimeout(debounce); } debounce = setTimeout(async () => { console.log('[watch] build started'); for (const ctx of contexts) { try { await ctx.cancel(); await ctx.rebuild(); } catch (error) { console.error('[watch]', error); } } console.log('[watch] build finished'); }, 100); }; watcher.subscribe(REPO_ROOT, (err, events) => { for (const event of events) { console.log(`File change detected: ${event.path}`); } rebuild(); }, { ignore: [ `**/.git/**`, `**/.simulation/**`, `**/test/outcome/**`, `.vscode-test/**`, `**/.venv/**`, `**/dist/**`, `**/node_modules/**`, `**/*.txt`, `**/baseline.json`, `**/baseline.old.json`, `**/*.w.json`, '**/*.sqlite', '**/*.sqlite-journal', 'test/aml/out/**' ] }); rebuild(); } else { await Promise.all([ esbuild.build(nodeExtHostBuildOptions), esbuild.build(webExtHostBuildOptions), esbuild.build(nodeSimulationBuildOptions), esbuild.build(nodeSimulationWorkbenchUIBuildOptions), esbuild.build(nodeExtHostSimulationTestOptions), esbuild.build(typeScriptServerPluginBuildOptions), esbuild.build(webviewBuildOptions), ]); // Move source maps to separate directory so they're not packaged with the extension await moveSourceMapsToSeparateDir(); } } function applyPackageJsonPatch(isPreRelease: boolean) { const packagejsonPath = path.join(import.meta.dirname, './package.json'); const json = JSON.parse(fs.readFileSync(packagejsonPath).toString()); const newProps: any = { buildType: 'prod', isPreRelease, }; const patchedPackageJson = Object.assign(json, newProps); // Remove fields which might reveal our development process delete patchedPackageJson['scripts']; delete patchedPackageJson['devDependencies']; delete patchedPackageJson['dependencies']; fs.writeFileSync(packagejsonPath, JSON.stringify(patchedPackageJson)); } main(); ================================================ FILE: .eslint-ignore ================================================ node_modules dist coverage lint-staged.config.js vite.config.ts **/vscode.proposed.*.ts **/vscode.d.ts .esbuild/extension.esbuild.ts test/simulation/fixtures/** test/scenarios/** .simulation/** .eslintplugin/** chat-lib/** test/aml/out/** .vscode-test/** # ignore vs src/util/vs/** # ignore test fixtures src/platform/parser/test/node/fixtures/** src/extension/test/node/fixtures/** src/extension/prompts/node/test/fixtures/** # TypeScript server plugin src/extension/typescriptContext/serverPlugin/fixtures/** src/extension/typescriptContext/serverPlugin/lib/** src/extension/typescriptContext/serverPlugin/dist/** # Ignore Built test-extension .vscode/extensions/test-extension/dist/** ================================================ FILE: .eslintplugin/index.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import type { LooseRuleDefinition } from '@typescript-eslint/utils/ts-eslint'; import * as glob from 'glob'; import path from 'path'; // Re-export all .ts files as rules const rules: Record = {}; await Promise.all( glob.sync('*.ts', { cwd: import.meta.dirname }) .filter(file => !file.endsWith('index.ts') && !file.endsWith('utils.ts')) .map(async file => { rules[path.basename(file, '.ts')] = (await import('./' + file)).default; }) ); export { rules }; ================================================ FILE: .eslintplugin/no-bad-gdpr-comment.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; export default new class NoBadGDPRComment implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { ['Program'](node) { for (const comment of (node as eslint.AST.Program).comments) { if (comment.type !== 'Block' || !comment.loc) { continue; } if (!comment.value.includes('__GDPR__')) { continue; } const dataStart = comment.value.indexOf('\n'); const data = comment.value.substring(dataStart) let gdprData: { [key: string]: object } | undefined try { const jsonRaw = `{ ${data} }` gdprData = JSON.parse(jsonRaw); } catch (e) { context.report({ loc: { start: comment.loc.start, end: comment.loc.end }, message: 'GDPR comment is not valid JSON' }); } if (gdprData) { const len = Object.keys(gdprData).length; if (len !== 1) { context.report({ loc: { start: comment.loc.start, end: comment.loc.end }, message: `GDPR comment must contain exactly one key, not ${Object.keys(gdprData).join(', ')}` }); } } } } }; } }; ================================================ FILE: .eslintplugin/no-funny-filename.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; import { readdirSync } from 'fs'; import path from 'path'; export default new class NoTestOnly implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { // compute/use real file path because LSP eslint might see cached // filenames from VSCode that aren't using the latest casing let realFilename: string | undefined; const filename = path.basename(context.filename); const filenames = readdirSync(path.dirname(context.filename)); for (const name of filenames) { if (name.toLowerCase() === filename.toLowerCase()) { realFilename = name; break; } } if (!realFilename) { throw new Error(`Filename not found ${filename}`) } // const filename = path.basename(context.filename); const idx = realFilename.indexOf('.'); const realFilenameName = idx !== -1 ? realFilename.substring(0, idx) : realFilename; const regex = /^[a-z0-9-]+([A-Z0-9-][a-z0-9-]*)*$/; if (!regex.test(realFilenameName)) { context.report({ loc: { line: 0, column: 0, }, message: `Filename '${realFilename}' should be in camelCase.`, }); } return {}; } }; ================================================ FILE: .eslintplugin/no-gdpr-event-name-mismatch.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { TSESTree } from '@typescript-eslint/typescript-estree'; import * as eslint from 'eslint'; export default new class NoGDPREventNameMismatch implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { type: "problem", fixable: "code", docs: { description: "Finds common cases where the gdpr comment does not match the telemetry event name in code.", }, }; create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { function getParentOfType(node: TSESTree.Node, type: string): NodeType | undefined { let parentNode: TSESTree.Node | undefined = node.parent; while (parentNode && parentNode.type !== type) { parentNode = parentNode.parent; } return parentNode as NodeType; } function getEventNameFromLeadingGdprComment(esNode: TSESTree.Node): string | undefined { const statement = getParentOfType(esNode, 'ExpressionStatement'); if (!statement) { return; } const comments = context.sourceCode.getCommentsBefore(statement as any); if (comments.length === 0) { return; } const comment = comments[0]; if (!comment.value.includes('__GDPR__') || !comment.loc) { return; } const dataStart = comment.value.indexOf('\n'); const data = comment.value.substring(dataStart) let gdprData: { [key: string]: object } try { const jsonRaw = `{ ${data} }` gdprData = JSON.parse(jsonRaw); } catch (e) { return; } return Object.keys(gdprData)[0]; } return { ['ExpressionStatement MemberExpression Identifier[name=/^send.*TelemetryEvent$/]'](node: any) { const esNode = node as TSESTree.Identifier; const gdprCommentEventName = getEventNameFromLeadingGdprComment(esNode); if (!gdprCommentEventName) { return; } const callExpr = getParentOfType(esNode, 'CallExpression'); if (!callExpr) { return; } const firstArg = callExpr.arguments[0]; if (firstArg.type !== TSESTree.AST_NODE_TYPES.Literal) { return; } const callName = firstArg.value if (callName !== gdprCommentEventName) { context.report({ node, message: `Found mismatch between GDPR comment event name (${gdprCommentEventName}) and telemetry event name (${callName}).` }); } } }; } }; ================================================ FILE: .eslintplugin/no-instanceof-uri.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; export default new class NoInstanceofUri implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { type: "problem", fixable: "code", docs: { description: "Disallow using 'instanceof URI', use 'URI.isURI' instead", }, messages: { noInstanceofURI: "Use 'URI.isUri()' instead of 'instanceof URI'. 'instanceof' is an issue because there are multiple URI classes in this codebase." } }; create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { BinaryExpression(node: any) { if (node.operator === 'instanceof' && node.right.type === 'Identifier' && node.right.name.toUpperCase() === 'URI') { context.report({ node, messageId: 'noInstanceofURI', fix: (fixer) => { return fixer.replaceText(node, `URI.isUri(${context.sourceCode.getText(node.left)})`); } }); } } }; } }; ================================================ FILE: .eslintplugin/no-missing-linebreak.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; export default new class MissingTSXLinebreak implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { type: "problem", fixable: "code", hasSuggestions: true, } create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { ['JSXText']: (node: any) => { let text = node.value; if (typeof text !== 'string') { return {}; } let jsxText = text; let index = node.range[0]; // Remove leading linebreaks const match = jsxText.match(/^\r?\n/); if (match) { jsxText = jsxText.slice(match[0].length); index += match[0].length; } const errorLocs = []; let lastFragment = ''; let linebreak = jsxText.match(/\r?\n/); while (linebreak?.[0]) { const linebreakLoc = jsxText.indexOf(linebreak[0]); index += linebreakLoc + linebreak[0].length; const fragment = jsxText.slice(0, linebreakLoc); if (!fragment) { break; } lastFragment = fragment; jsxText = jsxText.slice(linebreakLoc + linebreak[0].length); errorLocs.push(index - 1); linebreak = jsxText.match(/\r?\n/); } if (errorLocs.length < 2) { return; // All text is on one line } if (lastFragment.trim().length === 0) { // Last fragment is whitespace, it might be followed by another JSX element, which we already auto insert linebreaks for const nextChild = context.sourceCode.getTokenAfter(node); if (!nextChild || nextChild?.value === '<') { errorLocs.pop(); } } for (const errorLoc of errorLocs) { context.report({ loc: context.sourceCode.getLocFromIndex(errorLoc), message: "Use `
` linebreak to enforce newline in TSX string literal. Whitespace is removed from TSX during transpilation.", fix: (fixer) => { return fixer.insertTextAfterRange([errorLoc, errorLoc], '
'); } }); } } }; } }; ================================================ FILE: .eslintplugin/no-nls-localize.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; import { createImportRuleListener } from './utils.ts'; export default new class implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { messages: { noNlsLocalize: 'Do not import localize from nls. Use vscode.l10n.t or import l10n from @vscode/l10n instead.' }, docs: { description: 'Disallow importing localize from nls files', }, schema: [] }; create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return createImportRuleListener((node, path) => { // Match paths ending with /nls, /nls.js, /vs/nls, etc. if (path.endsWith('/nls') || path.endsWith('/nls.js') || path === 'vs/nls') { context.report({ loc: node.parent!.loc, messageId: 'noNlsLocalize' }); } }); } }; ================================================ FILE: .eslintplugin/no-restricted-copilot-pr-string.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; export default new class NoBadGDPRComment implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { type: 'problem', docs: { description: 'Ensure "Generate with Copilot" string in GitHubPullRequestProviders is never changed', category: 'Best Practices' }, schema: [ { type: 'object', properties: { className: { type: 'string' }, string: { type: 'string' } }, additionalProperties: false } ] } create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { const options = context.options[0] || {}; const className = options.className || 'GitHubPullRequestProviders'; const requiredString = options.string || 'Copilot'; let inTargetClass = false; return { ClassDeclaration(node) { if (node.id && node.id.name === className) { inTargetClass = true; } }, 'ClassDeclaration:exit'(node) { if (node.id && node.id.name === className) { inTargetClass = false; } }, Literal(node) { if (inTargetClass && typeof node.value === 'string' && node.value.includes('Generate')) { if (!node.value.includes(requiredString)) { context.report({ node, message: `String literal in ${className} must include the word "Copilot" as the string is referenced in the GitHub Pull Request extension. Talk to alexr00 if you need to change it.` }); } } } }; } }; ================================================ FILE: .eslintplugin/no-runtime-import.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { TSESTree } from '@typescript-eslint/typescript-estree'; import * as eslint from 'eslint'; import { dirname, join, relative } from 'path'; import picomatch from 'picomatch'; import { createImportRuleListener } from './utils.ts'; export default new class implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { messages: { layerbreaker: 'You are only allowed to import {{import}} from here using `import type ...`.' }, schema: { type: "array", items: { type: "object", additionalProperties: { type: "array", items: { type: "string" } } } } }; create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { let fileRelativePath = relative(dirname(import.meta.dirname), context.getFilename()); fileRelativePath = fileRelativePath.replace(/\\/g, '/'); if (!fileRelativePath.endsWith('/')) { fileRelativePath += '/'; } const ruleArgs = context.options[0] as Record; const matchingKey = Object.keys(ruleArgs).find(key => fileRelativePath.startsWith(key) || picomatch(key)(fileRelativePath)); if (!matchingKey) { // nothing return {}; } const restrictedImports = ruleArgs[matchingKey]; return createImportRuleListener((node, path) => { if (path[0] === '.') { path = join(dirname(context.getFilename()), path); } if (restrictedImports.includes(path) && !( (node.parent?.type === TSESTree.AST_NODE_TYPES.ImportDeclaration && node.parent.importKind === 'type') || (node.parent && 'exportKind' in node.parent && node.parent.exportKind === 'type'))) { // the export could be multiple types context.report({ loc: node.parent!.loc, messageId: 'layerbreaker', data: { import: path } }); } }); } }; ================================================ FILE: .eslintplugin/no-test-imports.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; import { dirname, join } from 'path'; import { createImportRuleListener } from './utils.ts'; export default new class implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = {}; create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { if (context.filename.includes('/test/')) { return {}; } return createImportRuleListener((node, path) => { if (path[0] === '.') { path = join(dirname(context.filename), path); } if (path.includes('/test/')) { context.report({ loc: node.parent!.loc, message: 'You are not allowed to import anything form /test/ file in a non-test file.', data: { import: path } }); } }); } }; ================================================ FILE: .eslintplugin/no-test-only.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; export default new class NoTestOnly implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { ['MemberExpression[object.name=/^(test|suite)$/][property.name="only"]']: (node: any) => { return context.report({ node, message: 'only is a dev-time tool and CANNOT be pushed' }); } }; } }; ================================================ FILE: .eslintplugin/no-unexternalized-strings.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; import * as eslint from 'eslint'; import type * as ESTree from 'estree'; function isStringLiteral(node: TSESTree.Node | ESTree.Node | null | undefined): node is TSESTree.StringLiteral { return !!node && node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string'; } function isDoubleQuoted(node: TSESTree.StringLiteral): boolean { return node.raw[0] === '"' && node.raw[node.raw.length - 1] === '"'; } /** * Enable bulk fixing double-quoted strings to single-quoted strings with the --fix eslint flag * * Disabled by default as this is often not the desired fix. Instead the string should be localized. However it is * useful for bulk conversations of existing code. */ const enableDoubleToSingleQuoteFixes = false; export default new class NoUnexternalizedStrings implements eslint.Rule.RuleModule { private static _rNlsKeys = /^[_a-zA-Z0-9][ .\-_a-zA-Z0-9]*$/; readonly meta: eslint.Rule.RuleMetaData = { messages: { doubleQuoted: 'Only use double-quoted strings for externalized strings.', badKey: 'The key \'{{key}}\' doesn\'t conform to a valid localize identifier.', duplicateKey: 'Duplicate key \'{{key}}\' with different message value.', badMessage: 'Message argument to \'{{message}}\' must be a string literal.' }, schema: false, fixable: enableDoubleToSingleQuoteFixes ? 'code' : undefined, }; create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { const externalizedStringLiterals = new Map(); const doubleQuotedStringLiterals = new Set(); function collectDoubleQuotedStrings(node: ESTree.Literal) { if (isStringLiteral(node) && isDoubleQuoted(node)) { doubleQuotedStringLiterals.add(node); } } function visitLocalizeCall(node: TSESTree.CallExpression) { // localize(key, message) const [keyNode, messageNode] = node.arguments; // (1) // extract key so that it can be checked later let key: string | undefined; if (isStringLiteral(keyNode)) { doubleQuotedStringLiterals.delete(keyNode); key = keyNode.value; } else if (keyNode.type === AST_NODE_TYPES.ObjectExpression) { for (const property of keyNode.properties) { if (property.type === AST_NODE_TYPES.Property && !property.computed) { if (property.key.type === AST_NODE_TYPES.Identifier && property.key.name === 'key') { if (isStringLiteral(property.value)) { doubleQuotedStringLiterals.delete(property.value); key = property.value.value; break; } } } } } if (typeof key === 'string') { let array = externalizedStringLiterals.get(key); if (!array) { array = []; externalizedStringLiterals.set(key, array); } array.push({ call: node, message: messageNode }); } // (2) // remove message-argument from doubleQuoted list and make // sure it is a string-literal doubleQuotedStringLiterals.delete(messageNode); if (!isStringLiteral(messageNode)) { context.report({ loc: messageNode.loc, messageId: 'badMessage', data: { message: context.getSourceCode().getText(node as ESTree.Node) } }); } } function visitL10NCall(node: TSESTree.CallExpression) { // localize(key, message) const [messageNode] = (node as TSESTree.CallExpression).arguments; // remove message-argument from doubleQuoted list and make // sure it is a string-literal if (isStringLiteral(messageNode)) { doubleQuotedStringLiterals.delete(messageNode); } else if (messageNode.type === AST_NODE_TYPES.ObjectExpression) { for (const prop of messageNode.properties) { if (prop.type === AST_NODE_TYPES.Property) { if (prop.key.type === AST_NODE_TYPES.Identifier && prop.key.name === 'message') { doubleQuotedStringLiterals.delete(prop.value); break; } } } } } function reportBadStringsAndBadKeys() { // (1) // report all strings that are in double quotes for (const node of doubleQuotedStringLiterals) { context.report({ loc: node.loc, messageId: 'doubleQuoted', fix: enableDoubleToSingleQuoteFixes ? (fixer) => { // Get the raw string content, unescaping any escaped quotes const content = (node as ESTree.SimpleLiteral).raw! .slice(1, -1) .replace(/(? 1) { for (let i = 1; i < values.length; i++) { if (context.getSourceCode().getText(values[i - 1].message as ESTree.Node) !== context.getSourceCode().getText(values[i].message as ESTree.Node)) { context.report({ loc: values[i].call.loc, messageId: 'duplicateKey', data: { key } }); } } } } } return { ['Literal']: (node: ESTree.Literal) => collectDoubleQuotedStrings(node), ['ExpressionStatement[directive] Literal:exit']: (node: TSESTree.Literal) => doubleQuotedStringLiterals.delete(node), // localize(...) ['CallExpression[callee.type="MemberExpression"][callee.object.name="nls"][callee.property.name="localize"]:exit']: (node: TSESTree.CallExpression) => visitLocalizeCall(node), // localize2(...) ['CallExpression[callee.type="MemberExpression"][callee.object.name="nls"][callee.property.name="localize2"]:exit']: (node: TSESTree.CallExpression) => visitLocalizeCall(node), // vscode.l10n.t(...) ['CallExpression[callee.type="MemberExpression"][callee.object.property.name="l10n"][callee.property.name="t"]:exit']: (node: TSESTree.CallExpression) => visitL10NCall(node), // l10n.t(...) ['CallExpression[callee.object.name="l10n"][callee.property.name="t"]:exit']: (node: TSESTree.CallExpression) => visitL10NCall(node), ['CallExpression[callee.name="localize"][arguments.length>=2]:exit']: (node: TSESTree.CallExpression) => visitLocalizeCall(node), ['CallExpression[callee.name="localize2"][arguments.length>=2]:exit']: (node: TSESTree.CallExpression) => visitLocalizeCall(node), ['Program:exit']: reportBadStringsAndBadKeys, }; } }; ================================================ FILE: .eslintplugin/no-unlayered-files.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; import path from 'path'; const layers = new Set([ 'common', 'vscode', 'node', 'vscode-node', 'worker', 'vscode-worker', ]) export default new class NoUnlayeredFiles implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { const filenameParts = context.filename.split(path.sep); if (!filenameParts.find(part => layers.has(part))) { context.report({ loc: { line: 0, column: 0, }, message: `File '${context.filename}' should be inside a '${[...layers].join(', ')}' folder.`, }); } return {}; } }; ================================================ FILE: .eslintplugin/package.json ================================================ { "name": "vscode-copilot-chat-eslint-plugin", "private": true, "type": "module" } ================================================ FILE: .eslintplugin/tsconfig.json ================================================ { "compilerOptions": { "target": "es2024", "lib": ["ES2024"], "module": "esnext", "allowImportingTsExtensions": true, "erasableSyntaxOnly": true, "verbatimModuleSyntax": true, "noEmit": true, "strict": true, "exactOptionalPropertyTypes": false, "useUnknownInCatchVariables": false, "noUnusedLocals": true, "noUnusedParameters": true }, "exclude": ["node_modules/**"] } ================================================ FILE: .eslintplugin/utils.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { TSESTree } from '@typescript-eslint/typescript-estree'; import * as eslint from 'eslint'; export function createImportRuleListener(validateImport: (node: TSESTree.Literal, value: string) => any): eslint.Rule.RuleListener { function _checkImport(node: TSESTree.Node | null) { if (node && node.type === 'Literal' && typeof node.value === 'string') { validateImport(node, node.value); } } return { // import ??? from 'module' ImportDeclaration: (node: any) => { _checkImport((node as TSESTree.ImportDeclaration).source); }, // import('module').then(...) OR await import('module') ['CallExpression[callee.type="Import"][arguments.length=1] > Literal']: (node: any) => { _checkImport(node); }, // import foo = ... ['TSImportEqualsDeclaration > TSExternalModuleReference > Literal']: (node: any) => { _checkImport(node); }, // export ?? from 'module' ExportAllDeclaration: (node: any) => { _checkImport((node as TSESTree.ExportAllDeclaration).source); }, // export {foo} from 'module' ExportNamedDeclaration: (node: any) => { _checkImport((node as TSESTree.ExportNamedDeclaration).source); }, }; } ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf # Enable Git LFS for SQLite database files *.sqlite filter=lfs diff=lfs merge=lfs -text ================================================ FILE: .github/CODENOTIFY ================================================ # Model prompts src/extension/prompts/node/agent/openAIPrompts.tsx @dileepyavan @kcutler src/extension/prompts/node/agent/anthropicPrompts.tsx @bhavyaus @bryanchen-d src/extension/prompts/node/agent/geminiPrompts.tsx @vijayupadya @pwang347 src/extension/prompts/node/agent/xAIPrompts.tsx @pwang347 @vijayupadya src/extension/prompts/node/agent/vscModelPrompts.tsx @karthiknadig @eleanorjboyd ================================================ FILE: .github/CODEOWNERS ================================================ # Ensure Lad and Joao review cache relevant code .github/workflows/pr-check-cache.yml @lszomoru @joaomoreno build/pr-check-cache-files.ts @lszomoru @joaomoreno test/base/cache-cli.ts @lszomoru @joaomoreno test/base/cache.ts @lszomoru @joaomoreno ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Copilot Chat Issues url: https://github.com/microsoft/vscode/issues/new?labels=chat-oss-issue about: Please file issues related to Copilot Chat in the VS Code repository. - name: Responsible AI Service response blocking url: https://github.com/microsoft/vscode/issues/253130 about: See meta-issue for information on RAI response blocking. - name: Request Rate Limiting url: https://github.com/microsoft/vscode/issues/253124 about: See meta-issue for scenarios where chat requests are blocked due to rate limiting. - name: Public Code Matching url: https://github.com/microsoft/vscode/issues/253129 about: Learn how to enable/disable public code matching in responses via your organizational settings. ================================================ FILE: .github/commands.json ================================================ [ { "type": "comment", "name": "question", "action": "updateLabels", "addLabel": "*question" }, { "type": "comment", "name": "dev-question", "action": "updateLabels", "addLabel": "*dev-question" }, { "type": "label", "name": "*question", "action": "close", "reason": "not_planned", "comment": "We closed this issue because it is a question about using VS Code rather than an issue or feature request. Please search for help on [StackOverflow](https://aka.ms/vscodestackoverflow), where the community has already answered thousands of similar questions. You may find their [guide on asking a new question](https://aka.ms/vscodestackoverflowquestion) helpful if your question has not already been asked. See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting).\n\nHappy Coding!" }, { "type": "label", "name": "*dev-question", "action": "close", "reason": "not_planned", "comment": "We have a great extension developer community over on [GitHub discussions](https://github.com/microsoft/vscode-discussions/discussions) and [Slack](https://vscode-dev-community.slack.com/) where extension authors help each other. This is a great place for you to ask questions and find support.\n\nHappy Coding!" }, { "type": "label", "name": "*extension-candidate", "action": "close", "reason": "not_planned", "comment": "We try to keep VS Code lean and we think the functionality you're asking for is great for a VS Code extension. Maybe you can already find one that suits you in the [VS Code Marketplace](https://aka.ms/vscodemarketplace). Just in case, in a few simple steps you can get started [writing your own extension](https://aka.ms/vscodewritingextensions). See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting).\n\nHappy Coding!" }, { "type": "label", "name": "*not-reproducible", "action": "close", "reason": "not_planned", "comment": "We closed this issue because we are unable to reproduce the problem with the steps you describe. Chances are we've already fixed your problem in a recent version of VS Code. If not, please ask us to reopen the issue and provide us with more detail. Our [issue reporting guidelines](https://aka.ms/vscodeissuereporting) might help you with that.\n\nHappy Coding!" }, { "type": "label", "name": "*out-of-scope", "action": "close", "reason": "not_planned", "comment": "We closed this issue because we [don't plan to address it](https://aka.ms/vscode-out-of-scope) in the foreseeable future. If you disagree and feel that this issue is crucial: we are happy to listen and to reconsider.\n\nIf you wonder what we are up to, please see our [roadmap](https://aka.ms/vscoderoadmap) and [issue reporting guidelines](https://aka.ms/vscodeissuereporting).\n\nThanks for your understanding, and happy coding!" }, { "type": "label", "name": "wont-fix", "action": "close", "reason": "not_planned", "comment": "We closed this issue because we [don't plan to address it](https://github.com/microsoft/vscode/wiki/Issue-Grooming#wont-fix-bugs).\n\nThanks for your understanding, and happy coding!" }, { "type": "comment", "name": "causedByExtension", "action": "updateLabels", "addLabel": "*caused-by-extension" }, { "type": "label", "name": "*caused-by-extension", "action": "close", "reason": "not_planned", "comment": "This issue is caused by an extension, please file it with the repository (or contact) the extension has linked in its overview in VS Code or the [marketplace](https://aka.ms/vscodemarketplace) for VS Code. See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). If you don't know which extension is causing the problem, you can run `Help: Start extension bisect` from the command palette (F1) to help identify the problem extension.\n\nHappy Coding!" }, { "type": "label", "name": "*as-designed", "action": "close", "reason": "not_planned", "comment": "The described behavior is how it is expected to work. If you disagree, please explain what is expected and what is not in more detail. See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting).\n\nHappy Coding!" }, { "type": "label", "name": "L10N", "assign": [ "csigs", "TylerLeonhardt" ] }, { "type": "comment", "name": "duplicate", "action": "updateLabels", "addLabel": "*duplicate" }, { "type": "label", "name": "*duplicate", "action": "close", "reason": "not_planned", "comment": "Thanks for creating this issue! We figured it's covering the same as another one we already have. Thus, we closed this one as a duplicate. You can search for [similar existing issues](${duplicateQuery}). See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting).\n\nHappy Coding!" }, { "type": "comment", "name": "verified", "action": "updateLabels", "addLabel": "verified", "removeLabel": "author-verification-requested", "requireLabel": "author-verification-requested", "disallowLabel": "unreleased" }, { "type": "comment", "name": "confirm", "action": "updateLabels", "addLabel": "confirmed", "removeLabel": "confirmation-pending" }, { "type": "comment", "name": "confirmationPending", "action": "updateLabels", "addLabel": "confirmation-pending", "removeLabel": "confirmed" }, { "type": "comment", "name": "needsMoreInfo", "action": "updateLabels", "addLabel": "~info-needed" }, { "type": "comment", "name": "needsPerfInfo", "addLabel": "info-needed", "comment": "Thanks for creating this issue regarding performance! Please follow this guide to help us diagnose performance issues: https://github.com/microsoft/vscode/wiki/Performance-Issues \n\nHappy Coding!" }, { "type": "comment", "name": "jsDebugLogs", "action": "updateLabels", "addLabel": "info-needed", "comment": "Please collect trace logs using the following instructions:\n\n> If you're able to, add `\"trace\": true` to your `launch.json` and reproduce the issue. The location of the log file on your disk will be written to the Debug Console. Share that with us.\n>\n> ⚠️ This log file will not contain source code, but will contain file paths. You can drop it into https://microsoft.github.io/vscode-pwa-analyzer/index.html to see what it contains. If you'd rather not share the log publicly, you can email it to connor@xbox.com" }, { "type": "comment", "name": "closedWith", "action": "close", "reason": "completed", "addLabel": "unreleased" }, { "type": "comment", "name": "spam", "action": "close", "reason": "not_planned", "addLabel": "invalid" }, { "type": "comment", "name": "a11ymas", "action": "updateLabels", "addLabel": "a11ymas" }, { "type": "label", "name": "*off-topic", "action": "close", "reason": "not_planned", "comment": "Thanks for creating this issue. We think this issue is unactionable or unrelated to the goals of this project. Please follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting).\n\nHappy Coding!" }, { "type": "comment", "name": "gifPlease", "action": "comment", "addLabel": "info-needed", "comment": "Thanks for reporting this issue! Unfortunately, it's hard for us to understand what issue you're seeing. Please help us out by providing a screen recording showing exactly what isn't working as expected. While we can work with most standard formats, `.gif` files are preferred as they are displayed inline on GitHub. You may find https://gifcap.dev helpful as a browser-based gif recording tool.\n\nIf the issue depends on keyboard input, you can help us by enabling screencast mode for the recording (`Developer: Toggle Screencast Mode` in the command palette). Lastly, please attach this file via the GitHub web interface as emailed responses will strip files out from the issue.\n\nHappy coding!" }, { "type": "comment", "name": "confirmPlease", "action": "comment", "addLabel": "info-needed", "comment": "Please diagnose the root cause of the issue by running the command `F1 > Help: Troubleshoot Issue` and following the instructions. Once you have done that, please update the issue with the results.\n\nHappy Coding!" }, { "__comment__": "Allows folks on the team to label issues by commenting: `\\label My-Label` ", "type": "comment", "name": "label" }, { "type": "comment", "name": "assign" }, { "type": "label", "name": "*workspace-trust-docs", "action": "close", "reason": "not_planned", "comment": "This issue appears to be the result of the new workspace trust feature shipped in June 2021. This security-focused feature has major impact on the functionality of VS Code. Due to the volume of issues, we ask that you take some time to review our [comprehensive documentation](https://aka.ms/vscode-workspace-trust) on the feature. If your issue is still not resolved, please let us know." }, { "type": "label", "name": "~verification-steps-needed", "action": "updateLabels", "addLabel": "verification-steps-needed", "removeLabel": "~verification-steps-needed", "comment": "Friendly ping! Looks like this issue requires some further steps to be verified. Please provide us with the steps necessary to verify this issue." }, { "type": "label", "name": "~info-needed", "action": "updateLabels", "addLabel": "info-needed", "removeLabel": "~info-needed", "comment": "Thanks for creating this issue! We figured it's missing some basic information or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). Please take the time to review these and update the issue.\n\nHappy Coding!" }, { "type": "label", "name": "~version-info-needed", "action": "updateLabels", "addLabel": "info-needed", "removeLabel": "~version-info-needed", "comment": "Thanks for creating this issue! We figured it's missing some basic information, such as a version number, or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). Please take the time to review these and update the issue.\n\nHappy Coding!" }, { "type": "label", "name": "~confirmation-needed", "action": "updateLabels", "addLabel": "info-needed", "removeLabel": "~confirmation-needed", "comment": "Please diagnose the root cause of the issue by running the command `F1 > Help: Troubleshoot Issue` and following the instructions. Once you have done that, please update the issue with the results.\n\nHappy Coding!" } ] ================================================ FILE: .github/copilot-instructions.md ================================================ # GitHub Copilot Chat Extension - Copilot Instructions ## Project Overview This is the **GitHub Copilot Chat** extension for Visual Studio Code - a VS Code extension that provides conversational AI assistance, a coding agent with many tools, inline editing capabilities, and advanced AI-powered features for VS Code. ### Key Features - **Chat Interface**: Conversational AI assistance with chat participants, variables, and slash commands - **Inline Chat**: AI-powered editing directly in the editor with `Ctrl+I` - **Agent Mode**: Multi-step autonomous coding tasks - **Edit Mode**: Natural language to code - **Inline Suggestions**: Next edit suggestions and inline completions - **Language Model Integration**: Support for multiple AI models (GPT-4, Claude, Gemini, etc.) - **Context-Aware**: Workspace understanding, semantic search, and code analysis ### Tech Stack - **TypeScript**: Primary language (follows VS Code coding standards) - **TSX**: Prompts are built using the @vscode/prompt-tsx library - **Node.js**: Runtime for extension host and language server features - **WebAssembly**: For performance-critical parsing and tokenization - **VS Code Extension API**: Extensive use of proposed APIs for chat, language models, and editing - **ESBuild**: Bundling and compilation - **Vitest**: Unit testing framework - **Python**: For notebooks integration and ML evaluation scripts ## Validating changes You MUST check compilation output before running ANY script or declaring work complete! 1. **ALWAYS** check the `start-watch-tasks` watch task output for compilation errors 2. **NEVER** use the `compile` task as a way to check if everything is working properly 3. **FIX** all compilation errors before moving forward ### TypeScript compilation steps - Monitor the `start-watch-tasks` task outputs for real-time compilation errors as you make changes - This task runs `npm: watch:tsc-extension`,`npm: watch:tsc-extension-web`, `npm: watch:tsc-simulation-workbench`, and `npm: watch:esbuild` to incrementally compile the project - Start the task if it's not already running in the background ## Project Architecture ### Top-Level Directory Structure #### Core Source Code (`src/`) - **`src/extension/`**: Main extension implementation, organized by feature - **`src/platform/`**: Shared platform services and utilities - **`src/util/`**: Common utilities, VS Code API abstractions, and service infrastructure #### Build & Configuration - **`.esbuild.ts`**: Build configuration for bundling extension, web worker, and simulation workbench - **`tsconfig.json`**: TypeScript configuration extending base config with React JSX settings - **`vite.config.ts`**: Test configuration for Vitest unit tests - **`package.json`**: Extension manifest with VS Code contributions, dependencies, and scripts #### Testing & Simulation - **`test/`**: Comprehensive test suite including unit, integration, and simulation tests - **`script/simulate.sh`**: Test runner for scenario-based testing - **`notebooks/`**: Jupyter notebooks for performance analysis and ML experiments #### Assets & Documentation - **`assets/`**: Icons, fonts, and visual resources - **`CONTRIBUTING.md`**: Architecture documentation and development guide ### Key Source Directories #### `src/extension/` - Feature Implementation **Core Chat & Conversation Features:** - **`conversation/`**: Chat participants, agents, and conversation flow orchestration - **`inlineChat/`**: Inline editing features (`Ctrl+I`) and hints system - **`inlineEdits/`**: Advanced inline editing capabilities with streaming edits **Context & Intelligence:** - **`context/`**: Context resolution for code understanding and workspace analysis - **`contextKeys/`**: VS Code context key management for UI state - **`intents/`**: Chat participant/slash command implementations - **`prompts/`**: Prompt engineering and template system - **`prompt/`**: Common prompt utilities - **`typescriptContext/`**: TypeScript-specific context and analysis **Search & Discovery:** - **`search/`**: General search functionality within the extension - **`workspaceChunkSearch/`**: Chunked workspace search for large codebases - **`workspaceSemanticSearch/`**: Semantic search across workspace content - **`workspaceRecorder/`**: Recording and tracking workspace interactions **Authentication & Configuration:** - **`authentication/`**: GitHub authentication and token management - **`configuration/`**: Settings and configuration management - **`byok/`**: Bring Your Own Key (BYOK) functionality for custom API keys **AI Integration & Endpoints:** - **`endpoint/`**: AI service endpoints and model selection - **`tools/`**: Language model tools and integrations - **`api/`**: Core API abstractions and interfaces - **`mcp/`**: Model Context Protocol integration **Development & Testing:** - **`testing/`**: Test generation and execution features - **`test/`**: Extension-specific test utilities and helpers **User Interface & Experience:** - **`commands/`**: Service for working with VS Code commands - **`codeBlocks/`**: Streaming code block processing - **`linkify/`**: URL and reference linkification - **`getting-started/`**: Onboarding and setup experience - **`onboardDebug/`**: Debug onboarding flows - **`survey/`**: User feedback and survey collection **Specialized Features:** - **`notebook/`**: Notebook integration and support - **`review/`**: Code review and PR integration features - **`renameSuggestions/`**: AI-powered rename suggestions - **`ignore/`**: File and pattern ignore functionality - **`xtab/`**: Cross-tab communication and state management **Infrastructure & Utilities:** - **`extension/`**: Core extension initialization and lifecycle - **`log/`**: Logging infrastructure and utilities - **`telemetry/`**: Analytics and usage tracking **VS Code API Type Definitions:** - Multiple `vscode.proposed.*.d.ts` files for proposed VS Code APIs including chat, language models, embeddings, and various editor integrations #### `src/platform/` - Platform Services - **`chat/`**: Core chat services and conversation options - **`openai/`**: OpenAI API protocol integration and request handling - **`embedding/`**: Vector embeddings for semantic search - **`parser/`**: Code parsing and AST analysis - **`search/`**: Workspace search and indexing - **`telemetry/`**: Analytics and usage tracking - **`workspace/`**: Workspace understanding and file management - **`notebook/`**: Notebook integration - **`git/`**: Git integration and repository analysis #### `src/util/` - Infrastructure - **`common/`**: Shared utilities, service infrastructure, and abstractions - **`vs/`**: Utilities borrowed from the microsoft/vscode repo (readonly) ### Extension Activation Flow 1. **Base Activation** (`src/extension/extension/vscode/extension.ts`): - Checks VS Code version compatibility - Creates service instantiation infrastructure - Initializes contribution system 2. **Service Registration**: - Platform services (search, parsing, telemetry, etc.) - Extension-specific services (chat, authentication, etc.) - VS Code integrations (commands, providers, etc.) 3. **Contribution Loading**: - Chat participants - Language model providers - Command registrations - UI contributions (views, menus, etc.) ### Chat System Architecture #### Chat Participants - **Default Agent**: Main conversational AI assistant - **Setup Agent**: Handles initial Copilot setup and onboarding - **Workspace Agent**: Specialized for workspace-wide operations - **Agent Mode**: Autonomous multi-step task execution #### Request Processing 1. **Input Parsing**: Parse user input for participants, variables, slash commands 2. **Context Resolution**: Gather relevant code context, diagnostics, workspace info 3. **Prompt Construction**: Build prompts with context and intent detection 4. **Model Interaction**: Send requests to appropriate language models 5. **Response Processing**: Parse and interpret AI responses 6. **Action Execution**: Apply code edits, show results, handle follow-ups #### Language Model Integration - Support for multiple providers (OpenAI, Anthropic, etc.) - Model selection and switching capabilities - Quota management and fallback handling - Custom instruction integration ### Inline Chat System - **Hint System**: Smart detection of natural language input for inline suggestions - **Intent Detection**: Automatic detection of user intent (explain, fix, refactor, etc.) - **Context Collection**: Gather relevant code context around cursor/selection - **Streaming Edits**: Real-time application of AI-suggested changes - **Version 2**: New implementation with improved UX and hide-on-request functionality ## Coding Standards ### TypeScript/JavaScript Guidelines - **Indentation**: Use **tabs**, not spaces - **Naming Conventions**: - `PascalCase` for types and enum values - `camelCase` for functions, methods, properties, and local variables - Use descriptive, whole words in names - **Strings**: - "double quotes" for user-visible strings that need localization - 'single quotes' for internal strings - **Functions**: Use arrow functions `=>` over anonymous function expressions - **Conditionals**: Always use curly braces, opening brace on same line - **Comments**: Use JSDoc style for functions, interfaces, enums, and classes ### React/JSX Conventions - Custom JSX factory: `vscpp` (instead of React.createElement) - Fragment factory: `vscppf` - Components follow VS Code theming and styling patterns ### Architecture Patterns - **Service-oriented**: Heavy use of dependency injection via `IInstantiationService` - **Contribution-based**: Modular system where features register themselves - **Event-driven**: Extensive use of VS Code's event system and disposables - **Layered**: Clear separation between platform services and extension features ### Testing Standards - **Unit Tests**: Vitest for isolated component testing - **Integration Tests**: VS Code extension host tests for API integration - **Simulation Tests**: End-to-end scenario testing with `.stest.ts` files - **Fixtures**: Comprehensive test fixtures for various scenarios ### File Organization - **Logical Grouping**: Features grouped by functionality, not technical layer - **Platform Separation**: Different implementations for web vs. Node.js environments - **Test Proximity**: Tests close to implementation (`/test/` subdirectories) - **Clear Interfaces**: Strong interface definitions for service boundaries ## Key Development Guidelines ### Arrow Functions and Parameters - Use arrow functions `=>` over anonymous function expressions - Only surround arrow function parameters when necessary: ```javascript x => x + x // ✓ Correct (x, y) => x + y // ✓ Correct (x: T, y: T) => x === y // ✓ Correct (x) => x + x // ✗ Wrong ``` ### Code Structure - Always surround loop and conditional bodies with curly braces - Open curly braces always go on the same line as whatever necessitates them - An open curly brace MUST be followed by a newline, with the body indented on the next line - Parenthesized constructs should have no surrounding whitespace - Single space follows commas, colons, and semicolons ```javascript for (let i = 0, n = str.length; i < 10; i++) { if (x < 10) { foo(); } } function f(x: number, y: string): void { } ``` ### Type Management - Do not export `types` or `functions` unless you need to share it across multiple components - Do not introduce new `types` or `values` to the global namespace - Use proper types. Do not use `any` unless absolutely necessary. - Use `readonly` whenever possible. - Avoid casts in TypeScript unless absolutely necessary. If you get type errors after your changes, look up the types of the variables involved and set up a proper system of types and interfaces instead of adding type casts. - Do not use `any` or `unknown` as the type for variables, parameters, or return values unless absolutely necessary. If they need type annotations, they should have proper types or interfaces defined. ## Key APIs and Integrations ### VS Code Proposed APIs (Enabled) The extension uses numerous proposed VS Code APIs for advanced functionality: - `chatParticipantPrivate`: Private chat participant features - `languageModelSystem`: System messages for LM API - `chatProvider`: Custom chat provider implementation - `mappedEditsProvider`: Advanced editing capabilities - `inlineCompletionsAdditions`: Enhanced inline suggestions - `aiTextSearchProvider`: AI-powered search capabilities ### External Integrations - **GitHub**: Authentication and API access - **Azure**: Cloud services and experimentation - **OpenAI**: Language model API - **Anthropic**: Claude model integration - See **[src/extension/agents/claude/AGENTS.md](../src/extension/agents/claude/AGENTS.md)** for complete Claude Agent SDK integration documentation including architecture, components, and registries - **Telemetry**: Usage analytics and performance monitoring ## Development Workflow ### Setup and Build - `npm install`: Install dependencies - `npm run compile`: Development build - `npm run watch:*`: Various watch modes for development ### Updating Dependencies **Anthropic SDK Packages:** When updating `@anthropic-ai/claude-agent-sdk` or `@anthropic-ai/sdk`, you **MUST** follow the upgrade guide in **[src/extension/agents/claude/AGENTS.md](../src/extension/agents/claude/AGENTS.md#upgrading-anthropic-sdk-packages)**. This includes: 1. Reviewing changelogs for breaking changes 2. Checking compilation errors in key Claude integration files 3. Running through the testing checklist for core functionality, tools, hooks, and slash commands ### Testing - `npm run test:unit`: Unit tests - `npm run test:extension`: VS Code integration tests - `npm run simulate`: Scenario-based simulation tests ### Key Entry Points for Edits **Chat & Conversation Features:** - **Adding new chat features**: Start in `src/extension/conversation/` - **Chat participants and agents**: Look in `src/extension/conversation/` for participant implementations - **Conversation storage**: Modify `src/extension/conversationStore/` for persistence features - **Inline chat improvements**: Look in `src/extension/inlineChat/` and `src/extension/inlineEdits/` **Context & Intelligence:** - **Context resolution changes**: Check `src/extension/context/` and `src/extension/typescriptContext/` - **Prompt engineering**: Update `src/extension/prompts/` and `src/extension/prompt/` - **Intent detection**: Modify `src/extension/intents/` for user intent classification **Search & Discovery:** - **Search functionality**: Update `src/extension/search/` for general search - **Workspace search**: Modify `src/extension/workspaceChunkSearch/` for large codebase search - **Semantic search**: Edit `src/extension/workspaceSemanticSearch/` for AI-powered search - **Workspace tracking**: Update `src/extension/workspaceRecorder/` for interaction recording **Authentication & Configuration:** - **Authentication flows**: Modify `src/extension/authentication/` for GitHub integration - **Settings and config**: Update `src/extension/configuration/` and `src/extension/settingsSchema/` - **BYOK features**: Edit `src/extension/byok/` for custom API key functionality **AI Integration:** - **AI endpoints**: Update `src/extension/endpoint/` for model selection and routing - **Language model tools**: Modify `src/extension/tools/` for AI tool integrations - **API abstractions**: Edit `src/extension/api/` for core interfaces - **MCP integration**: Update `src/extension/mcp/` for Model Context Protocol features **User Interface:** - **VS Code commands**: Update `src/extension/commands/` for command implementations - **Code block rendering**: Modify `src/extension/codeBlocks/` for code display - **Onboarding flows**: Edit `src/extension/getting-started/` and `src/extension/onboardDebug/` - **Cross-tab features**: Update `src/extension/xtab/` for multi-tab coordination **Testing & Development:** - **Test generation**: Modify `src/extension/testing/` for AI-powered test creation - **Extension tests**: Update `src/extension/test/` for extension-specific test utilities **Platform Services:** - **Core platform services**: Extend `src/platform/` services for cross-cutting functionality - **VS Code integration**: Update contribution files and extension activation code - **Configuration**: Modify `package.json` contributions for VS Code integration This extension is a complex, multi-layered system that provides comprehensive AI assistance within VS Code. Understanding the service architecture, contribution system, and separation between platform and extension layers is crucial for making effective changes. ## Best Practices - Use services and dependency injection over VS Code extension APIs when possible: - Use `IFileSystemService` instead of Node's `fs` or `vscode.workspace.fs` - Use `ILogService` instead of `console.log` - Look for existing `I*Service` interfaces before reaching for raw APIs - **Why**: Enables unit testing without VS Code host, supports simulation tests, provides cross-platform abstractions (Node vs web), and adds features like caching and size limits - Always use the URI type instead of using string file paths. There are many helpers available for working with URIs. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "devcontainers" directory: "/" groups: all: patterns: - "*" schedule: interval: "weekly" - package-ecosystem: "github-actions" directory: "/" groups: all: patterns: - "*" schedule: interval: "weekly" - package-ecosystem: "npm" directories: - "/" groups: all: patterns: - "*" ignore: - dependency-name: "@azure/identity" update-types: ["version-update:semver-major", "version-update:semver-minor"] # Keep @azure/identity because upgrading breaks automation - dependency-name: "@stylistic/eslint-plugin" update-types: ["version-update:semver-major"] # Keep @stylistic/eslint-plugin 3 because 4 requires eslint 9 (@jrieken) - dependency-name: "@hediet/node-reload" update-types: ["version-update:semver-major", "version-update:semver-minor"] # @hediet/node-reload 0.8 for source compatibility (@hediet) - dependency-name: "@types/eslint" update-types: ["version-update:semver-major"] # eslint 8 for json config (@jrieken) - dependency-name: "@types/node" update-types: ["version-update:semver-major"] # Keep Node compatibility aligned with current Electron version in VS Code - dependency-name: "@types/react" update-types: ["version-update:semver-major"] # @fluentui/react-components currently requires react <19 (@ulugbekna) - dependency-name: "@types/react-dom" update-types: ["version-update:semver-major"] # @fluentui/react-components currently requires react <19 (@ulugbekna) - dependency-name: "@types/vscode" # Manually sync with engine version - dependency-name: "@vitest/snapshot" update-types: ["version-update:semver-major"] # @vitest/snapshot 2 for source compatibility (@connor4312) - dependency-name: "eslint" update-types: ["version-update:semver-major"] # eslint 8 for json config (@jrieken) - dependency-name: "eslint-plugin-local" update-types: ["version-update:semver-major"] # eslint-plugin-local 1 for config compatibility (@jrieken) - dependency-name: "lint-staged" # lint-staged 15.2.9 because newer versions fail to retrieve staged changes (@ulugbekna) - dependency-name: "react" update-types: ["version-update:semver-major"] # @fluentui/react-components currently requires react <19 (@ulugbekna) - dependency-name: "react-dom" update-types: ["version-update:semver-major"] # @fluentui/react-components currently requires react <19 (@ulugbekna) - dependency-name: "@vscode/tree-sitter-wasm" update-types: ["version-update:semver-major", "version-update:semver-minor"] # @vscode/tree-sitter-wasm 0.0.5 because extension tests fail with newer versions (@alexr00) - dependency-name: "monaco-editor" update-types: ["version-update:semver-major", "version-update:semver-minor"] # monaco-editor 0.44.0 because the simulation workbench fails to launch (@alexdima @hediet) - dependency-name: "applicationinsights" update-types: ["version-update:semver-major"] # applicationinsights 2 for source compatibility (@lramos15) - dependency-name: "web-tree-sitter" update-types: ["version-update:semver-major", "version-update:semver-minor"] # web-tree-sitter 0.23 for source compatibility (@alexr00) schedule: interval: "weekly" ================================================ FILE: .github/instructions/prompt-tsx.instructions.md ================================================ --- applyTo: '**/*.tsx' description: Prompt-TSX coding guidelines --- Guidelines for TSX files using [prompt-tsx](https://github.com/microsoft/vscode-prompt-tsx) focusing on specific patterns and token budget management for AI prompt engineering. ## Component Structure ### Base Pattern - Extend `PromptElement` or `PromptElement` for all prompt components - Props interfaces must extend `BasePromptElementProps` ```tsx interface MyPromptProps extends BasePromptElementProps { readonly userQuery: string; } class MyPrompt extends PromptElement { render() { return ( <> ... {this.props.userQuery} ); } } ``` ### Async Components - The `render` method can be async for components that need to perform async operations - All async work should be done directly in the `render` method ```tsx class FileContextPrompt extends PromptElement { async render() { const fileContent = await readFileAsync(this.props.filePath); return ( <> File content: {fileContent} ); } } ``` ## Prompt-Specific JSX ### Line Breaks - **CRITICAL**: Use `
` for line breaks - newlines are NOT preserved in JSX - Never rely on whitespace or string literal newlines ```tsx // ✅ Correct You are an AI assistant.
Follow these guidelines.
// ❌ Wrong - newlines will be collapsed You are an AI assistant. Follow these guidelines. ``` ## Priority System ### Priority Values - Higher numbers = higher priority (like z-index) - Use consistent ranges: - System messages: 1000 - User queries: 900 - Recent history: 700-800 - Context/attachments: 600-700 - Background info: 0-500 ```tsx ... {query} ``` ### Flex Properties for Token Budget - `flexGrow={1}` - expand to fill remaining token space - `flexReserve` - reserve tokens before rendering - `passPriority` - pass-through containers that don't affect child priorities ```tsx ``` ## Content Handling ### TextChunk for Truncation - Use `TextChunk` for content that may exceed token budget - Set `breakOn` patterns for intelligent truncation ```tsx {longUserQuery} {documentContent} ``` ### Tag Component for Structured Content - Use `Tag` for XML-like structured content with attributes - Validates tag names and properly formats attributes ```tsx {content} ``` ## References and Metadata ### Prompt References - Use `` for tracking variable usage - Use `` for metadata that survives pruning ```tsx ``` ### Keep-With Pattern - Use `useKeepWith()` for content that should be pruned together ```tsx const KeepWith = useKeepWith(); return ( <> ... ... ); ``` ## Token Budget Management ### Sizing-Aware Rendering - Use `PromptSizing` parameter for budget-aware content generation - Implement cooperative token usage ```tsx async render(sizing: PromptSizing): Promise { const content = await this.generateContent(sizing.tokenBudget); return <>{content}; } ``` ### Performance - Avoid expensive work in `render` methods when possible - Cache computations when appropriate - Use async `render` for all async operations ================================================ FILE: .github/instructions/vitest-unit-tests.instructions.md ================================================ --- applyTo: '**/*.spec.ts' description: Vitest unit testing guidelines --- Please follow these guidelines when writing unit tests using Vitest. These tests are `*.spec.ts` ## Best Practices - Prefer explicit Test/Mock classes over mutating real instances or creating adhoc one-off mocks. - Never use `as any` to override private methods or assign properties on real objects. - Mock versions of services are typically named `Mock*` or `Test*`, you can search to find whether one already exists. - Some examples: `MockFileSystemService`, `MockChatResponseStream`, `TestTasksService`. - If there is no preexisting implementation of a service that is appropriate to reuse in the test, then you can create a simple mock or stub implementation in a file under a `test/` folder near the interface definition. - A mock class should be configurable so that it can be shared and set up for different test scenarios. - The helper `createExtensionUnitTestingServices` returns a `TestingServiceCollection` preconfigured with some common mock services, use `IInstantiationService` to create instances with those mocks. Here's an example of using it properly ```ts const serviceCollection = store.add(createExtensionUnitTestingServices()); instantiationService = serviceCollection.createTestingAccessor().get(IInstantiationService); const mockFs = accessor.get(IFileSystemService) as MockFileSystemService; const testService = instantiationService.createInstance(SomeServiceToTest); ``` - When asked to write new tests, add tests to cover the behavior of the code under test, especially things that are interesting, unexpected, or edge cases. - Avoid adding tests that simply repeat existing tests or cover trivial code paths. - If available, prefer the runTests tool to run tests over a terminal command. - Keep tests deterministic and fast. - Avoid starting real servers or performing network I/O. - Avoid excessive repetition in tests, use `beforeEach` to set up common state. - Use helper functions to encapsulate common test logic. ================================================ FILE: .github/prompts/updateCopilotCLIToolMapping.prompt.md ================================================ --- name: updateCopilotCLIToolMapping description: Update the mapping of Copilot CLI tools from the source code for the CLI runtime --- The constant `ToolFriendlyNameAndHandlers` in src/extension/chatSessions/copilotcli/common/copilotCLITools.ts contains a mapping of known tools, and how the progress and output is displayed. The type `ToolInfo` contains all of the tools and their corresponding arguments/return types. All of this information has been derived from /src/tools/** I would like you to update the `ToolFriendlyNameAndHandlers` mapping as well as `ToolInfo` and other related types based on any new/updated tools that are defined in the CLI runtime repo. * You must create simple TypeScript interfaces as done today see `WebSearchTool` for the tool `web_search` * You must have an entry in `ToolFriendlyNameAndHandlers` for the new tools * You must add/update any of the related tests At the end of all of your changes you must provide a summary * List of updated tools and their friendly names What are the updates to, did the arguments change, did the return type change, etc. How does this impact what is displayed to the user * List of new tools and their friendly names How are the arguments and output displayed to the user Finally, if the user hasn't already provided this, then you must ask for the folder path to the CLI runtime repo. ================================================ FILE: .github/prompts/updateGithubCopilotSDK.prompt.md ================================================ --- name: updateGithubCopilotSDK description: Use this to update the Github Copilot CLI/SDK model: Claude Opus 4.6 --- You are an expert at upgrading the @github/copilot npm package in the vscode-copilot-chat project. ## Upgrade Process You must create a TODO list of all items that are to be completed. You MUST create a TODO markdown file before commencing any of the work. Update this file after each step is completed. You must also use the update_todo tool on each step. Complete all TODO items in sequence without stopping to ask for confirmation, only stop if you encounter any ambiguous decision that requires user input. The TODO is your primary tracking mechanism. Before each step you MUST read the TODO to determine what to do next. At a minimum your TODO must contain the following: 1. Snapshot old type definitions 2. Update the package 3. Compare differences in type definitions and document them 4. Compile, 5. fix 6. test 7. Repease steps Compile, fix and tests until all tests are passing 8. Run integration tests 9. Repeate Compile, Fix, Test, Test integration tests until all integration tests are passing 11. Create a summary Follow these steps exactly: ### 1. Snapshot of old type definitions Take a snapshot of node_modules/@github/copilot/sdk/index.d.ts to compare against after the upghttps://github.com/microsoft/vscode/issues/291457rade. ### 2. Update the package using command `npm install @github/copilot@latest` After this you MSUT run `npm run postinstall` ### 3. Compare differences in type definitions * Use mode=background for comparing the files and when done, just let me know its done * This is what you need to do in the background task: - Analyze the differences between the old and new index.d.ts files to identify any API changes, new features, or breaking changes. - Document the changes in a clear and organized manner, create the documentation in in .build/upgrade-notes.md ### 4. Compile, fix and test #### 4.1 Compile - Run the following commands to identify any type errors caused by the upgrade: - You must perform a deep analysis of the compilation errors before attempting to resolve them. - Ensure there are no compilation errors before proceeding to run the tests. ```bash npm run compile npx tsc --noEmit --project tsconfig.json ``` #### 4.2 Run Tests - Use the following command to run test ```bash npm run test:unit ``` - Do NOT change the behavour of the code just to make the tests pass. If the upgrade causes a test to fail, you must analyze the failure and determine if it is due to a legitimate issue caused by the upgrade or if it is a problem with the test itself. - Ensure all tests are passing before proceeding to the next step. ### 5. Running integration tests - The tests are located in test/e2e/cli.stest.ts. - The tests in this file are all skipped by default using `suite.skip`, so you must remove the `.skip` to enable them before running the tests. - Run the tests using the following command: ```bash npm run simulate -- --grep=@cli --verbose -n=1 -p=1@cli ``` These tests are very slow, you might have to wait for around 5 minutes for them to complete. - As earlier, fix the test failures without changing the behavior of the code, and ensure that all tests are passing. NOTE: Tests are considered passing only if you get a score of 100% Here's a sample output. As you can see below the score needs to be 100/100 for the tests to be considered passing. ``` Suite Summary by Language: ┌─────────┬───────────────────┬──────────┬───────┬────────────┬──────────┐ │ (index) │ Suite │ Language │ Model │ # of tests │ Score(%) │ ├─────────┼───────────────────┼──────────┼───────┼────────────┼──────────┤ │ 0 │ '@cli [external]' │ '-' │ '-' │ 16 │ 100 │ └─────────┴───────────────────┴──────────┴───────┴────────────┴──────────┘ Approximate Summary (due to using --n=1 instead of --n=10): Overall Approximate Score: 100.00 / 100 ``` #### 6. Re-introduce `stest.skip` changes in cli.stest.ts #### 5. Summarize the changes - After successfully upgrading the @github/copilot package and ensuring that all tests are passing, you must create a summary of the changes that were made during the upgrade process. - Give a summary of the changes in the code base - Give a summary of the changes in the tests - Give a summary of the differenes in the type definitions between the old and new versions of the @github/copilot package. - Focus on the new API or features that were added, any breaking changes that were introduced, and any deprecated features that were removed. - Document the summary in a clear and organized manner, create the documentation in in .build/upgrade-notes.md ================================================ FILE: .github/workflows/copilot-setup-steps.yml ================================================ name: Copilot Setup Steps # Automatically run the setup steps when they are changed to allow for easy validation, and # allow manual testing through the repository's "Actions" tab on: workflow_dispatch: push: paths: - .github/workflows/copilot-setup-steps.yml pull_request: paths: - .github/workflows/copilot-setup-steps.yml jobs: copilot-setup-steps: name: Setup Development Environment runs-on: vscode-large-runners steps: - name: Checkout repository uses: actions/checkout@v6 with: lfs: true - name: Setup Node.js uses: actions/setup-node@v6 with: node-version-file: .nvmrc - name: Setup Python uses: actions/setup-python@v6 with: python-version: '3.12' architecture: 'x64' - name: Setup .NET uses: actions/setup-dotnet@v5 with: dotnet-version: '10.0' - name: Install setuptools run: pip install setuptools - name: Restore build cache uses: actions/cache/restore@v4 id: build-cache with: key: build_cache-${{ hashFiles('build/.cachesalt', 'package-lock.json') }} path: .build/build_cache - name: Extract build cache if: steps.build-cache.outputs.cache-hit == 'true' run: tar -xzf .build/build_cache/cache.tgz - name: Install dependencies if: steps.build-cache.outputs.cache-hit != 'true' run: npm ci - name: Create build cache archive if: steps.build-cache.outputs.cache-hit != 'true' run: | set -e mkdir -p .build node build/listBuildCacheFiles.js .build/build_cache_list.txt mkdir -p .build/build_cache tar -czf .build/build_cache/cache.tgz --files-from .build/build_cache_list.txt - name: Verify installation run: | node --version npm --version python --version dotnet --version ================================================ FILE: .github/workflows/ensure-node-modules-cache.yml ================================================ name: Ensure node modules cache on: push: branches: - main permissions: contents: read jobs: linux: name: Linux runs-on: [ self-hosted, 1ES.Pool=1es-vscode-ubuntu-22.04-x64 ] steps: - name: Checkout repository uses: actions/checkout@v6 with: lfs: true - name: Setup Node.js uses: actions/setup-node@v6 with: node-version-file: .nvmrc - name: Restore build cache uses: actions/cache@v4 id: build-cache with: key: build_cache-${{ hashFiles('build/.cachesalt', 'package-lock.json') }} path: .build/build_cache - name: Install dependencies if: steps.build-cache.outputs.cache-hit != 'true' run: npm ci - name: Create build cache archive if: steps.build-cache.outputs.cache-hit != 'true' run: | set -e mkdir -p .build node build/listBuildCacheFiles.js .build/build_cache_list.txt mkdir -p .build/build_cache tar -czf .build/build_cache/cache.tgz --files-from .build/build_cache_list.txt windows: name: Windows runs-on: [ self-hosted, 1ES.Pool=1es-vscode-windows-2022-x64 ] steps: - name: Checkout repository uses: actions/checkout@v6 with: lfs: true - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: '22.21.x' - name: Setup Python uses: actions/setup-python@v6 with: python-version: '3.12' architecture: 'x64' - name: Restore build cache uses: actions/cache@v4 id: build-cache with: key: windows-build_cache-${{ hashFiles('build/.cachesalt', 'package-lock.json') }} path: .build/build_cache - name: Install dependencies if: steps.build-cache.outputs.cache-hit != 'true' run: npm ci - name: Create build cache archive if: steps.build-cache.outputs.cache-hit != 'true' run: | mkdir -Force .build node build/listBuildCacheFiles.js .build/build_cache_list.txt mkdir -Force .build/build_cache 7z.exe a .build/build_cache/cache.7z -mx3 `@.build/build_cache_list.txt ================================================ FILE: .github/workflows/npm-package.yml ================================================ name: chat-lib tests on: pull_request: workflow_dispatch: permissions: contents: read concurrency: group: chat-lib-tests-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: test: name: chat-lib tests (${{ matrix.os }}) runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22.x cache: npm cache-dependency-path: | package-lock.json chat-lib/package-lock.json - name: Extract chat-lib shell: bash run: | npm ci npm run extract-chat-lib rm -rf node_modules - name: Install chat-lib dependencies working-directory: chat-lib run: npm ci - name: Build chat-lib working-directory: chat-lib run: npm run build - name: Test chat-lib working-directory: chat-lib run: npm test ================================================ FILE: .github/workflows/pr.yml ================================================ name: PR Checks on: push: branches: - 'gh-readonly-queue/main/*' pull_request: branches: - main - 'release/*' permissions: contents: read pull-requests: read jobs: check-test-cache: name: Check test cache runs-on: [ self-hosted, 1ES.Pool=1es-vscode-ubuntu-22.04-x64 ] steps: - name: Checkout code uses: actions/checkout@v6 with: lfs: true - uses: actions/setup-node@v6 with: node-version-file: .nvmrc - name: Restore build cache uses: actions/cache/restore@v4 id: build-cache with: key: build_cache-${{ hashFiles('build/.cachesalt', 'package-lock.json') }} path: .build/build_cache - name: Extract build cache if: steps.build-cache.outputs.cache-hit == 'true' run: tar -xzf .build/build_cache/cache.tgz - name: Install dependencies if: steps.build-cache.outputs.cache-hit != 'true' run: npm ci - name: Ensure no duplicate cache keys run: npx tsx test/base/cache-cli check - name: Ensure no untrusted cache changes run: npx tsx build/pr-check-cache-files.ts if: github.event_name == 'pull_request' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPOSITORY: ${{ github.repository }} PULL_REQUEST: ${{ github.event.pull_request.number }} check-telemetry: name: Check telemetry events runs-on: [ self-hosted, 1ES.Pool=1es-vscode-ubuntu-22.04-x64 ] steps: - name: Checkout code uses: actions/checkout@v6 with: lfs: true - uses: actions/setup-node@v6 with: node-version: '22.21.x' - name: Validate telemetry events run: npx --package=@vscode/telemetry-extractor --yes vscode-telemetry-extractor -s . > /dev/null linux-tests: name: Test (Linux) runs-on: [ self-hosted, 1ES.Pool=1es-vscode-ubuntu-22.04-x64 ] steps: - name: Checkout repository uses: actions/checkout@v6 with: lfs: true - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: '22.21.x' - name: Setup Python uses: actions/setup-python@v6 with: python-version: '3.12' architecture: 'x64' - name: Setup .NET uses: actions/setup-dotnet@v5 with: dotnet-version: '10.0' - name: Install setuptools run: pip install setuptools - name: Restore build cache uses: actions/cache/restore@v4 id: build-cache with: key: build_cache-${{ hashFiles('build/.cachesalt', 'package-lock.json') }} path: .build/build_cache - name: Extract build cache if: steps.build-cache.outputs.cache-hit == 'true' run: tar -xzf .build/build_cache/cache.tgz - name: Install dependencies if: steps.build-cache.outputs.cache-hit != 'true' run: npm ci - name: Create build cache archive if: steps.build-cache.outputs.cache-hit != 'true' run: | set -e mkdir -p .build node build/listBuildCacheFiles.js .build/build_cache_list.txt mkdir -p .build/build_cache tar -czf .build/build_cache/cache.tgz --files-from .build/build_cache_list.txt - name: Ensure proposed API types are up to date run: npm run vscode-dts:check - name: TypeScript type checking run: npm run typecheck - name: Lint run: npm run lint - name: Compile run: npm run compile - name: Run vitest unit tests run: npm run test:unit - name: Run simulation tests with cache run: npm run simulate-ci - name: Run extension tests using VS Code run: xvfb-run -a npm run test:extension - name: Run Completions Core prompt tests run: npm run test:prompt - name: Run Completions Core lib tests using VS Code run: xvfb-run -a npm run test:completions-core - name: Archive simulation output if: always() run: | set -e mkdir -p .simulation-archive tar -czf .simulation-archive/simulation.tgz -C .simulation . - name: Upload simulation output if: always() uses: actions/upload-artifact@v5 with: name: simulation-output-linux-${{ github.run_attempt }} path: .simulation-archive/simulation.tgz windows-tests: name: Test (Windows) runs-on: [ self-hosted, 1ES.Pool=1es-vscode-windows-2022-x64 ] steps: - name: Checkout repository uses: actions/checkout@v6 with: lfs: true - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: '22.21.x' - name: Setup Python uses: actions/setup-python@v6 with: python-version: '3.12' architecture: 'x64' - name: Setup .NET uses: actions/setup-dotnet@v5 with: dotnet-version: '10.0' - name: Install setuptools run: pip install setuptools - name: Restore build cache uses: actions/cache/restore@v4 id: build-cache with: key: windows-build_cache-${{ hashFiles('build/.cachesalt', 'package-lock.json') }} path: .build/build_cache - name: Extract build cache if: steps.build-cache.outputs.cache-hit == 'true' run: 7z.exe x .build/build_cache/cache.7z -aoa - name: Install dependencies if: steps.build-cache.outputs.cache-hit != 'true' run: npm ci - name: Create build cache archive if: steps.build-cache.outputs.cache-hit != 'true' run: | mkdir -Force .build node build/listBuildCacheFiles.js .build/build_cache_list.txt mkdir -Force .build/build_cache 7z.exe a .build/build_cache/cache.7z -mx3 `@.build/build_cache_list.txt - name: TypeScript type checking run: npm run typecheck - name: Lint run: npm run lint - name: Compile run: npm run compile - name: Run vitest unit tests run: npm run test:unit - name: Run simulation tests with cache run: npm run simulate-ci - name: Run extension tests using VS Code run: npm run test:extension - name: Run Completions Core prompt tests run: npm run test:prompt - name: Run Completions Core lib tests using VS Code run: npm run test:completions-core ================================================ FILE: .gitignore ================================================ .DS_Store .vscode-test/ .vscode-test-web/ node_modules/ dist/ dist-sourcemaps/ # created by simulation .simulation # stores wasm and other build files .build # created by python scripts __pycache__/ .venv* .ruff_cache/ *.egg-info/ # Secret token files /.env *.token # Test infra test/simulation/language/harness/ # localization files are generated in the build l10n/ # vitest --coverage coverage/ # Base cache database (GC mode) test/simulation/cache/_base.sqlite test/aml/out # Ignore files in huksy (else we always end up with files when creating worktrees) .husky/_/ # claude .claude/settings.local.json # VS Code extension debug profile (agent-browser automation) .vscode-ext-debug/ # playwright .playwright-mcp/ # Chat customizations .agents/agents/*.local.md .claude/agents/*.local.md .github/agents/*.local.md .agents/agents/*.local.agent.md .claude/agents/*.local.agent.md .github/agents/*.local.agent.md .agents/hooks/*.local.json .claude/hooks/*.local.json .github/hooks/*.local.json .agents/instructions/*.local.instructions.md .claude/instructions/*.local.instructions.md .github/instructions/*.local.instructions.md .agents/prompts/*.local.prompt.md .claude/prompts/*.local.prompt.md .github/prompts/*.local.prompt.md .agents/skills/.local/ .claude/skills/.local/ .github/skills/.local/ ================================================ FILE: .husky/pre-commit ================================================ set -e npx lint-staged ================================================ FILE: .husky/pre-push ================================================ set -e # git-lfs hook command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/pre-push'.\n"; exit 2; } git lfs pre-push "$@" ================================================ FILE: .mocha-multi-reporters.js ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ const path = require('path'); // In CI, the name of the NPM command that was run is used to generate a unique // filename for the JUnit report. The name we give it is then used as the test // bundle name in DataDog. const commandToBundleName = { isolatedProxyTests: 'IsolatedProxy', reverseProxyTests: 'ReverseProxy', test: 'LSPClient', 'test:agent': 'Agent', 'test:lib': 'Lib', 'test:lib-e2e': 'LibEndToEnd', }; const config = { reporterEnabled: 'spec', }; if (process.env.CI) { const bundleName = commandToBundleName[process.env.npm_lifecycle_event] || 'Unit'; config.reporterEnabled += ', mocha-junit-reporter'; config.mochaJunitReporterReporterOptions = { testCaseSwitchClassnameAndName: true, testsuitesTitle: `Copilot ${bundleName} Tests`, mochaFile: path.resolve(__dirname, `test-results-${bundleName}.xml`), }; } module.exports = config; ================================================ FILE: .mocharc.js ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; const config = { exit: true, 'node-option': 'unhandled-rejections=strict', reporter: 'mocha-multi-reporters', 'reporter-option': [`configFile=${__dirname}/.mocha-multi-reporters.js`], require: ['tsx'], ui: 'tdd', }; const cmd = process.env.npm_lifecycle_event; if (['test:lsp-client'].includes(cmd) && !process.env.CI) { config.parallel = true; } if (['test:lsp-client', 'reverseProxyTests'].includes(cmd) && process.env.CI) { config.bail = true; } if (['test:lsp-client', 'test:lib-e2e'].includes(cmd) && process.env.CI) { config.retries = 3; } module.exports = config; ================================================ FILE: .npmrc ================================================ engine-strict=true ================================================ FILE: .nvmrc ================================================ 22.21.1 ================================================ FILE: .prettierignore ================================================ dist/** .vscode/** .vscode-test/** test/.vscode-test/** .git-blame-ignore-revs **/*.md **/*.ts **/*.js **/*.yml test/scenarios/** .simulation/** .build/** test/simulation/fixtures/** src/extension/prompts/node/test/fixtures/** test/simulation/baseline.json prompt/dist/** src/base/util/*.json ================================================ FILE: .vscode/conversation.schema.json ================================================ { "$schema": "http://json-schema.org/draft-07/schema", "type": "array", "definitions": { "keyword-list": { "type": "array", "items": { "type": [ "string", "object" ], "anyOf": [ { "type": "string" }, { "$ref": "#/definitions/keyword-object" } ] } }, "keyword-object": { "type": "object", "properties": { "allOf": { "description": "A list of keywords or keyword lists that must all be satisfied in the response for the test to pass.", "$ref": "#/definitions/keyword-list" }, "anyOf": { "description": "A list of keywords or keyword lists which at least one must be satisfied in the response for the test to pass.", "$ref": "#/definitions/keyword-list" }, "not": { "description": "A list of keywords or keyword lists that must not be satisfied in the response for the test to pass.", "$ref": "#/definitions/keyword-list" } } }, "keywords": { "anyOf": [ { "type": "array", "$ref": "#/definitions/keyword-list" }, { "type": "object", "$ref": "#/definitions/keyword-object" } ] } }, "items": { "type": "object", "required": [ "question" ], "properties": { "question": { "description": "The user question to send to Copilot.", "type": "string" }, "stateFile": { "description": "The relative path to the state file to use for this question.", "type": "string" }, "keywords": { "description": "The keywords to expect or exclude from the response.", "$ref": "#/definitions/keywords" } } }, "additionalProperties": false } ================================================ FILE: .vscode/extensions/test-extension/.vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "args": [ "--extensionDevelopmentPath=${workspaceFolder}/test-extension" ], "name": "Launch Simulation Test Runner", "outFiles": [ "${workspaceFolder}/.vscode/extensions/test-extension/dist/**/*.js" ], // "preLaunchTask": "npm", "request": "launch", "type": "extensionHost" } ] } ================================================ FILE: .vscode/extensions/test-extension/bootstrap.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; (globalThis).projectRoot = vscode.workspace.workspaceFolders?.at(0)?.uri.fsPath ?? __dirname; ================================================ FILE: .vscode/extensions/test-extension/main.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import './bootstrap'; import * as fs from 'fs'; import { join } from 'path'; import * as vscode from 'vscode'; import { AsyncIterableObject, AsyncIterableSource } from '../../../src/util/vs/base/common/async'; import { CancellationToken } from '../../../src/util/vs/base/common/cancellation'; import { DisposableStore } from '../../../src/util/vs/base/common/lifecycle'; import { URI } from '../../../src/util/vs/base/common/uri'; import { generateUuid } from '../../../src/util/vs/base/common/uuid'; import { IDetectedSuiteOutput, IDetectedTestOutput, OutputType, RunOutput } from '../../../test/simulation/shared/sharedTypes'; import { ISpawnSimulationOptions, SIMULATION_MAIN_PATH, extractJSONL, spawnSimulation } from '../../../test/simulation/workbench/utils/simulationExec'; import { REPO_ROOT } from '../../../test/simulation/workbench/utils/utils'; export async function activate(context: vscode.ExtensionContext) { // probe for project root let isCorrectRepo = false; try { const pkg = JSON.parse(String(await fs.promises.readFile(join(REPO_ROOT, 'package.json')))); isCorrectRepo = pkg.name === 'copilot-chat'; } catch (err) { console.error('[STEST] error reading ' + join(REPO_ROOT, 'package.json')); console.error(err); isCorrectRepo = false; } if (!isCorrectRepo) { console.log('[STEST] NO activation because in wrong REPO/WORKSPACE', REPO_ROOT); return; } const ctrl = vscode.tests.createTestController('simulation', 'STest'); ctrl.refreshHandler = async (token) => { const stream = spawnSimulation({ ignoreNonJSONLines: true, args: ['--list-tests', '--list-suites', '--json'] }, token); const suites = new Map(); for await (const item of stream) { const testItem = ctrl.createTestItem(item.name, item.name, item.location && URI.file(item.location.path)); testItem.range = item.location && new vscode.Range(item.location.position.line, item.location.position.character, item.location.position.line, item.location.position.character); if (item.type === OutputType.detectedSuite) { suites.set(item.name, testItem); ctrl.items.add(testItem); } else if (item.type === OutputType.detectedTest) { const suiteItem = suites.get(item.suiteName); (suiteItem?.children ?? ctrl.items).add(testItem); } } }; ctrl.refreshHandler(CancellationToken.None); const simulationAsTestRun = async (request: vscode.TestRunRequest, options: { extraArgs: string[]; debug?: boolean }, token: vscode.CancellationToken) => { const run = ctrl.createTestRun(request, undefined, false); const args = ['--json']; if (options.extraArgs.length) { args.push(...options.extraArgs); } const items = new Map(); const stack: vscode.TestItemCollection[] = []; if (request.include && request.include.length) { const grep: string[] = []; for (const item of request.include) { grep.push(item.label); items.set(item.label, item); stack.push(item.children); } args.push('--grep', grep.join('|')); } else { stack.push(ctrl.items); } while (stack.length > 0) { const coll = stack.pop()!; for (const [, item] of coll) { if (item.children.size > 0) { stack.push(item.children); } else { items.set(item.label, item); } } } if (request.exclude && request.exclude.length) { const omitGrep: string[] = []; for (const item of request.exclude) { omitGrep.push(item.label); items.delete(item.label); } args.push('--omit-grep', omitGrep.join('|')); } run.appendOutput('[STEST] will SPAWN simulation with: ' + args.join(' ') + '\r\n'); try { const stream = !options.debug ? spawnSimulation({ ignoreNonJSONLines: true, args }, token) : debugSimulation({ ignoreNonJSONLines: true, args }, token); class Runs { private _starts: number = 0; private _passes: number = 0; private _fails: number = 0; constructor(readonly n: number) { } get passes() { return this._passes; } get fails() { return this._fails; } start() { this._starts++; return this._starts === 1; } done(pass: boolean) { if (pass) { this._passes++; } else { this._fails++; } if (this._passes + this._fails === this.n) { return true; } } } const nRuns = new Map(); for await (const output of stream) { run.appendOutput('[STEST] received output:\r\n' + JSON.stringify(output, undefined, 2).replaceAll('\n', '\r\n') + '\r\n'); if (output.type === OutputType.initialTestSummary) { // mark tests as enqueued for (const item of output.testsToRun) { const test = items.get(item); if (test) { run.enqueued(test); nRuns.set(test, new Runs(output.nRuns)); } } } else if (output.type === OutputType.skippedTest) { // mark tests as skipped const test = items.get(output.name); if (test) { run.skipped(test); nRuns.delete(test); } } else if (output.type === OutputType.testRunStart) { // mark tests as running const test = items.get(output.name); const runs = test && nRuns.get(test)!; if (test && runs?.start()) { run.started(test); } } else if (output.type === OutputType.testRunEnd) { // mark tests as done, process output const test = items.get(output.name); if (!test) { continue; } const runs = nRuns.get(test)!; if (runs.done(output.pass)) { run.passed(test); run.appendOutput(`[STEST] DONE with ${output.name}, ${runs.passes} passes and ${runs.fails} fails`, undefined, test); } } else if (output.type === OutputType.deviceCodeCallback) { vscode.env.openExternal(vscode.Uri.parse(output.url)); } } } catch (err) { if (err instanceof Error && err.name !== 'Cancelled') { run.appendOutput('[STEST] FAILED to run\r\n'); run.appendOutput(String(err) + '\r\n'); } } finally { run.end(); } }; const defaultRunProfile = ctrl.createRunProfile('STest', vscode.TestRunProfileKind.Run, (request, token) => simulationAsTestRun(request, { extraArgs: ['-p', '20'], }, token)); context.subscriptions.push(defaultRunProfile); defaultRunProfile.isDefault = true; const defaultDebugProfile = ctrl.createRunProfile('STest: debug', vscode.TestRunProfileKind.Debug, (request, token) => simulationAsTestRun(request, { extraArgs: ['--n', '1', '-p', '1'], debug: true }, token)); context.subscriptions.push(defaultDebugProfile); const visualizeDebugProfile = ctrl.createRunProfile('STest: inspect and visualize', vscode.TestRunProfileKind.Debug, (request, token) => { const args = { fileName: request.include![0].uri!.fsPath, path: request.include![0].label, }; vscode.commands.executeCommand( 'debug-value-editor.debug-and-send-request', { launchConfigName: "Test Visualization Runner STests", args: args, revealAvailablePropertiesView: true, } ); }); context.subscriptions.push(visualizeDebugProfile); const updateBaselineProfile = ctrl.createRunProfile('STest: update-baseline', vscode.TestRunProfileKind.Run, (request, token) => simulationAsTestRun(request, { extraArgs: ['--update-baseline', '-p', '20'] }, token)); context.subscriptions.push(updateBaselineProfile); context.subscriptions.push(ctrl); } function debugSimulation(options: ISpawnSimulationOptions, token: CancellationToken): AsyncIterableObject { const source = new AsyncIterableSource(); const key = generateUuid(); const store = new DisposableStore(); const sessions = new Set(); // (1) launch Promise.resolve(vscode.debug.startDebugging(vscode.workspace.workspaceFolders![0], { type: 'node-terminal', request: 'launch', name: 'Debug Simulation Tests', command: `node ${SIMULATION_MAIN_PATH} ${options.args.map(a => a.includes(' ') ? `"${a}"` : a).join(' ')}`, __key: key, })).catch(err => source.reject(err)); token.onCancellationRequested(() => { sessions.forEach(s => !s.parentSession && vscode.debug.stopDebugging(s)); source.resolve(); }); //(2) spy store.add(vscode.debug.registerDebugAdapterTrackerFactory('*', { createDebugAdapterTracker: (session) => { if (sessions.has(session)) { return; } const __key = session.configuration.__key ?? session.parentSession?.configuration.__key; if (__key !== key) { return; } sessions.add(session); return { onDidSendMessage({ type, event, body }) { if (type === 'event' && event === 'output' && body.category === 'stdout') { source.emitOne(body.output); } } }; } })); store.add(vscode.debug.onDidTerminateDebugSession(session => { if (sessions.delete(session)) { source.resolve(); } })); source.asyncIterable.toPromise().finally(() => store.dispose()); return extractJSONL(source.asyncIterable, options); } ================================================ FILE: .vscode/extensions/test-extension/package.json ================================================ { "publisher": "ms-vscode", "name": "simulation-test-runner", "version": "0.0.4", "displayName": "STest Runner", "engines": { "vscode": "^1.85.0" }, "main": "./dist/simulation-extension.js", "activationEvents": [ "onStartupFinished" ] } ================================================ FILE: .vscode/extensions/visualization-runner/README.md ================================================ # Visualization Runner This extension add "Visualize Test" code actions to tests that have `[visualizable]` in their name: ![screenshot](./docs/screenshot.png) It runs them using file://./../../../test/testVisualizationRunner.ts. ================================================ FILE: .vscode/extensions/visualization-runner/entry.js ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ require('tsx/cjs'); const { enableHotReload, hotRequire } = require("@hediet/node-reload"); enableHotReload({ entryModule: module }); /** * @param {import("vscode").ExtensionContext} context */ function activate(context) { context.subscriptions.push(hotRequire(module, "./extension", ext => new ext.Extension())); } module.exports = { activate }; ================================================ FILE: .vscode/extensions/visualization-runner/extension.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as ts from 'typescript'; import { Range, languages, workspace } from 'vscode'; import { timeout } from '../../../src/util/vs/base/common/async'; import { CachedFunction, LRUCachedFunction } from '../../../src/util/vs/base/common/cache'; import { Disposable } from '../../../src/util/vs/base/common/lifecycle'; export class Extension extends Disposable { private readonly _testCaches = new CachedFunction((fileName: string) => { let first = true; return new LRUCachedFunction(async (modelVersion: number) => { if (first) { first = false; } else { await timeout(1000); } const document = workspace.textDocuments.find(d => d.fileName === fileName); if (document?.version !== modelVersion) { return []; } const text = document.getText(); if (!text.includes('test')) { return []; } const tests = getTests(text); return tests.filter(isVisualizableTest); }); }); constructor() { super(); this._register(languages.registerCodeLensProvider([ { language: 'javascript', scheme: 'file' }, { language: 'typescript', scheme: 'file' } ], { provideCodeLenses: async (document, token) => { const info = await (this._testCaches.get(document.fileName).get(document.version)); return info.map(t => ({ range: new Range(t.lineNumber, 0, t.lineNumber, 0), command: { title: 'Visualize Test', command: 'debug-value-editor.debug-and-send-request', arguments: [{ launchConfigName: "Test Visualization Runner", args: { fileName: document.fileName, path: t.path, }, revealAvailablePropertiesView: true, }], }, isResolved: true, })); }, })); } } function isVisualizableTest(info: TestInfo): boolean { return info.path.some(p => p.indexOf('[visualizable]') !== -1); } type TestInfo = { path: string[]; lineNumber: number; }; function getTests(document: string): TestInfo[] { let sf; try { sf = ts.createSourceFile('', document, ts.ScriptTarget.ESNext, true); } catch (e) { console.error(e); return []; } function parseTest(node: ts.Node): { testName: string; node: ts.Node } | undefined { if (!ts.isCallExpression(node)) { return undefined; } if (!ts.isIdentifier(node.expression)) { return undefined; } if (node.expression.text !== 'test') { return undefined; } const firstArg = node.arguments[0]; if (!ts.isStringLiteral(firstArg)) { return undefined; } return { testName: firstArg.text, node: node.expression, }; } function parseDescribeOrSuite(node: ts.Node): { describeName: string; node: ts.Node } | undefined { if (!ts.isCallExpression(node)) { return undefined; } if (!ts.isIdentifier(node.expression)) { return undefined; } if (node.expression.text !== 'describe' && node.expression.text !== 'suite') { return undefined; } const firstArg = node.arguments[0]; if (!ts.isStringLiteral(firstArg)) { return undefined; } return { describeName: firstArg.text, node: node.expression, }; } const currentPath: string[] = []; const result: TestInfo[] = []; function find(node: ts.Node) { const test = parseTest(node); if (test) { const pos = sf.getLineAndCharacterOfPosition(test.node.getStart()); result.push({ path: [...currentPath, test.testName], lineNumber: pos.line }); } const describe = parseDescribeOrSuite(node); if (describe) { currentPath.push(describe.describeName); } ts.forEachChild(node, find); if (describe) { currentPath.pop(); } } find(sf); return result; } ================================================ FILE: .vscode/extensions/visualization-runner/package.json ================================================ { "publisher": "ms-vscode", "name": "visualization-runner", "version": "0.1.0", "displayName": "Visualization Runner", "engines": { "vscode": "^1.85.0" }, "main": "./entry.js", "activationEvents": [ "onLanguage:typescript" ], "extensionDependencies": [ "ms-vscode.debug-value-editor" ] } ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "connor4312.esbuild-problem-matchers", "ms-vscode.extension-test-runner", "ms-vscode.debug-value-editor", "ms-vscode.web-editors", "ms-vscode.visualization-runner", "vitest.explorer", "ms-vscode.ts-file-path-support", "charliermarsh.ruff" ] } ================================================ FILE: .vscode/launch.json ================================================ { "version": "0.2.0", "configurations": [ { "type": "extensionHost", "request": "launch", "name": "Launch Copilot Extension", "runtimeExecutable": "${execPath}", "args": [ "--extensionDevelopmentPath=${workspaceFolder}" ], "smartStep": true, "sourceMaps": true, "envFile": "${workspaceFolder}/.env", "env": { "COPILOT_LOG_TELEMETRY": "true", "VSCODE_DEV_DEBUG": "1", }, "outFiles": [ "${workspaceFolder}/dist/**/*.js", "!**/node_modules/**" ], "presentation": { "group": "0_launch" } }, { "type": "extensionHost", "request": "launch", "name": "Launch Copilot Extension - Watch Mode", "runtimeExecutable": "${execPath}", "args": [ "--extensionDevelopmentPath=${workspaceFolder}" ], "smartStep": true, "sourceMaps": true, "preLaunchTask": "watch", "env": { "COPILOT_LOG_TELEMETRY": "true", "VSCODE_DEV_DEBUG": "1", }, "outFiles": [ "${workspaceFolder}/dist/**/*.js", "!**/node_modules/**" ], "presentation": { "group": "0_launch", } }, { "type": "node", "request": "launch", "name": "Launch Copilot Extension - Watch Mode - Code OSS", "runtimeExecutable": "${workspaceFolder}/../vscode/scripts/code.sh", "windows": { "runtimeExecutable": "${workspaceFolder}/../vscode/scripts/code.bat", }, "attachSimplePort": 5870, "args": [ "--extensionDevelopmentPath=${workspaceFolder}", ], "smartStep": true, "sourceMaps": true, "preLaunchTask": "watch", "env": { "COPILOT_LOG_TELEMETRY": "true", "VSCODE_DEV_DEBUG": "1", }, "outFiles": [ "${workspaceFolder}/dist/**/*.js", "!**/node_modules/**" ], "presentation": { "group": "0_launch", } }, { // See `launchConfigName` in file://./extensions/visualization-runner/extension.ts "name": "Test Visualization Runner", "type": "node", "request": "launch", "program": "${workspaceRoot}/test/testVisualizationRunner.ts", "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/tsx", "presentation": { "group": "1_launch" }, }, { // See `launchConfigName` in file://./extensions/test-extension/main.ts "name": "Test Visualization Runner STests", "type": "node", "request": "launch", "program": "${workspaceRoot}/test/testVisualizationRunnerSTest.ts", "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/tsx", "presentation": { "group": "1_launch" }, }, { "name": "Extension tests", "type": "extensionHost", "request": "launch", "testConfiguration": "${workspaceFolder}/.vscode-test.mjs", "sourceMaps": true, "smartStep": true, "envFile": "${workspaceFolder}/.env", "internalConsoleOptions": "openOnSessionStart", "outFiles": [ "${workspaceFolder}/dist/**/*.js", "!**/node_modules/**" ], "presentation": { "group": "1_launch" }, }, { "name": "Run simulation tests", "internalConsoleOptions": "openOnSessionStart", "runtimeExecutable": "npm", "args": [ "run", "simulate", "--", "-n=1", "-p=1" ], "request": "launch", "type": "node", "presentation": { "group": "1_launch" } }, { "name": "Run tool call s-test", "internalConsoleOptions": "openOnSessionStart", "runtimeExecutable": "npm", "args": [ "run", "simulate", "--", "--verbose", "-n=1", "-p=1", "--sidebar", "--scenario-test=toolcall.stest", "--external-scenarios=${workspaceFolder}/test/toolcalls", "--output=${workspaceFolder}/test/out/" ], "request": "launch", "type": "node", "presentation": { "group": "1_launch" } }, { "type": "node", "name": "Postinstall", "request": "launch", "runtimeExecutable": "npm", "args": [ "run", "postinstall" ], "presentation": { "group": "1_launch" } }, { "name": "Debug Simulation Workbench UI", "type": "chrome", "request": "launch", "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", "windows": { "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" }, "runtimeArgs": [ "${workspaceRoot}/script/electron/simulationWorkbenchMain.js", "--remote-debugging-port=9222" ], "webRoot": "${workspaceRoot}", "presentation": { "group": "1_launch" } }, { "name": "Simulation Workbench - Renderer process", "type": "chrome", "request": "attach", "webRoot": "${workspaceFolder}", "port": 9222, "presentation": { "group": "1_launch" }, }, { "name": "Simulation Workbench - Main process", "type": "node", "request": "launch", "cwd": "${workspaceFolder}", "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", "windows": { "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" }, "args": [ "${workspaceRoot}/script/electron/simulationWorkbenchMain.js", "--remote-debugging-port=9222" ], "autoAttachChildProcesses": true, "presentation": { "group": "1_launch" }, }, { "type": "node", "request": "launch", "name": "Debug Test Script", "runtimeExecutable": "npm", "args": [ "run", "test" ], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen" }, { "type": "node", "request": "launch", "name": "Debug chat-lib vitest", "cwd": "${workspaceFolder}/chat-lib", "runtimeExecutable": "${workspaceFolder}/chat-lib/node_modules/.bin/vitest", "runtimeArgs": [ "run", "--reporter=verbose" ], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "skipFiles": [ "/**" ], "presentation": { "group": "1_launch" } }, { "type": "node", "request": "launch", "name": "Debug vitest", "cwd": "${workspaceFolder}", "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/vitest", "runtimeArgs": [ "run", "--reporter=verbose" ], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "skipFiles": [ "/**" ], "presentation": { "group": "1_launch" } }, { "type": "extensionHost", "request": "launch", "name": "Launch Copilot Extension - TS Server in Debug Mode", "runtimeExecutable": "${execPath}", "args": [ "--extensionDevelopmentPath=${workspaceFolder}" ], "smartStep": true, "sourceMaps": true, "envFile": "${workspaceFolder}/.env", "env": { "COPILOT_LOG_TELEMETRY": "true", "TSS_REMOTE_DEBUG": "9223", "TSS_DEBUG": "9223", }, "outFiles": [ "${workspaceFolder}/dist/**/*.js", "!**/node_modules/**" ], "presentation": { "group": "2_launch" } }, { "name": "Attach to TypeScript Server", "type": "node", "request": "attach", "port": 9223, "outFiles": [ "${workspaceFolder}/node_modules/@vscode/copilot-typescript-server-plugin/dist/**/*.js", ], "presentation": { "group": "2_launch" } }, { "name": "Attach to Extension Host - Code OSS", "type": "node", "request": "attach", "restart": true, "timeout": 0, "port": 5870, "sourceMaps": true, "outFiles": [ "${workspaceFolder}/../vscode/out/**/*.js", "${workspaceFolder}/../vscode/extensions/*/out/**/*.js", "${workspaceFolder}/dist/**/*.js", "!**/node_modules/**" ], "presentation": { "group": "2_launch" } }, { "name": "Run Completions-Core Extension Tests", "type": "extensionHost", "request": "launch", "args": [ "--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/src/extension/completions-core/vscode-node/extension/test/run", "--disable-extensions" ], "env": { "TSX_TSCONFIG_PATH": "${workspaceFolder}/tsconfig.json", "VITEST": "true" }, "internalConsoleOptions": "openOnSessionStart", "outFiles": [ "${workspaceFolder}/**/*.ts", "${workspaceFolder}/dist/**/*.js", "!**/node_modules/**" ], "autoAttachChildProcesses": true, "presentation": { "group": "1_launch" } }, ], "compounds": [ { "name": "Simulation Workbench", "configurations": [ "Simulation Workbench - Main process", "Simulation Workbench - Renderer process" ], "stopAll": true, "presentation": { "group": "1_launch" } } ] } ================================================ FILE: .vscode/mcp.json ================================================ { "servers": { "vscode-playwright-mcp": { "type": "stdio", "command": "npm", // See https://github.com/microsoft/vscode/blob/main/test/mcp/README.md for supported arguments "args": [ "run", "start-stdio", "--", "--extensionDevelopmentPath=${workspaceFolder}", ], "cwd": "${workspaceFolder}/../vscode/test/mcp" } }, "inputs": [] } ================================================ FILE: .vscode/settings.json ================================================ { "files.trimTrailingWhitespace": true, "[typescript]": { "editor.insertSpaces": false, "editor.defaultFormatter": "vscode.typescript-language-features", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports": "always" } }, "[typescriptreact]": { "editor.insertSpaces": false, "editor.defaultFormatter": "vscode.typescript-language-features", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports": "always" } }, "[javascript]": { "editor.insertSpaces": false, "editor.defaultFormatter": "vscode.typescript-language-features", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports": "always" } }, "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "files.insertFinalNewline": false }, "[jsonc]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true }, "[yaml]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true }, "[markdown]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true }, "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports": "explicit" } }, "search.exclude": { "**/*.egg-info/**/*": true, "src/base/util/*.bpe": true, "src/base/util/tokenizer_*.json": true, "src/extension/chatSessions/vscode-node/test/fixtures/**": true, "src/extension/completions-core/dist": true, "src/extension/prompts/node/test/fixtures/**/*": true, "src/extension/test/node/fixtures/**/*": true, "src/platform/parser/test/node/fixtures/**/*": true, "test/simulation/fixtures": true }, "files.watcherExclude": { ".simulation": true, ".vscode-test": true, ".build": true, }, "files.readonlyInclude": { "src/util/vs/**": true, "test/simulation/cache/base.sqlite": true }, "git.branchProtection": [ "main", "release/*" ], "git.branchProtectionPrompt": "alwaysCommitToNewBranch", "git.branchRandomName.enable": true, "githubPullRequests.assignCreated": "${user}", "githubPullRequests.defaultMergeMethod": "squash", "githubPullRequests.ignoredPullRequestBranches": [ "main" ], "typescript.preferences.quoteStyle": "single", "typescript.format.enable": true, "typescript.format.insertSpaceAfterCommaDelimiter": true, "typescript.format.insertSpaceAfterSemicolonInForStatements": true, "typescript.format.insertSpaceBeforeAndAfterBinaryOperators": true, "typescript.format.insertSpaceAfterKeywordsInControlFlowStatements": true, "typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": true, "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false, "typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": false, "typescript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": true, "typescript.format.insertSpaceBeforeFunctionParenthesis": false, "typescript.format.placeOpenBraceOnNewLineForFunctions": false, "typescript.format.placeOpenBraceOnNewLineForControlBlocks": false, "json.schemas": [ { "fileMatch": [ "**/*.conversation.json" ], "url": "./.vscode/conversation.schema.json" }, { "fileMatch": [ "**/*.state.json" ], "url": "./.vscode/state.schema.json" } ], "git.detectSubmodules": false, "githubPullRequests.upstreamRemote": "never", "extension-test-runner.debugOptions": { "outFiles": [ "${workspaceFolder}/dist/**/*.js" ] }, "files.associations": { "**/*.conversation.json": "jsonc" }, "workbench.editorAssociations": { // Use the default diff editor instead of the default custom editor view. "{git,gitlens}:/**/*.w.json": "default", "*.workspaceRecording.jsonl": "web-editor" }, "editor.experimental.preferTreeSitter.typescript": false, "editor.experimental.preferTreeSitter.regex": false, "editor.experimental.preferTreeSitter.css": true, "mochaExplorer.files": "dist/test-unit.js", "mochaExplorer.ui": "tdd", "mochaExplorer.timeout": 5000, "web-editors.editorTypes": { "ast.w": "https://microsoft.github.io/vscode-web-editor-text-tools/?editor=ast-viewer", "textRange.w": "https://microsoft.github.io/vscode-web-editor-text-tools/?editor=selection-editor", "jsonUi.w": "https://microsoft.github.io/vscode-web-editor-json-ui/", "diff.w": "https://microsoft.github.io/vscode-web-editor-text-tools/?editor=diff", "text.w": "https://microsoft.github.io/vscode-web-editor-text-tools/?editor=text", "recording.w.json": "https://microsoft.github.io/vscode-workbench-recorder-viewer/", "scoredEdits.w.json": "https://microsoft.github.io/vscode-workbench-recorder-viewer/?editRating", "workspaceRecording.jsonl": "https://microsoft.github.io/vscode-workbench-recorder-viewer/?jsonl=true", }, "explorer.fileNesting.patterns": { "vscode.d.ts": "vscode.proposed.*.ts", }, "git.diagnosticsCommitHook.enabled": true, "git.diagnosticsCommitHook.sources": { "*": "error", "ts": "warning", "eslint": "warning" }, "githubPullRequests.codingAgent.enabled": true, "githubPullRequests.codingAgent.uiIntegration": true, "python-envs.defaultEnvManager": "ms-python.python:system", "python-envs.pythonProjects": [], "chat.tools.terminal.autoApprove": { "npx vitest": true }, "chat.agentSkillsLocations": { ".github/skills/.local": true, ".agents/skills/.local": true, ".claude/skills/.local": true, } } ================================================ FILE: .vscode/snippets.code-snippets ================================================ { "copyright": { "prefix": "copyright", "body": [ "/*---------------------------------------------------------------------------------------------", " * Copyright (c) Microsoft Corporation. All rights reserved.", " * Licensed under the MIT License. See License.txt in the project root for license information.", " *--------------------------------------------------------------------------------------------*/", "", "" ] } } ================================================ FILE: .vscode/state.schema.json ================================================ { "$schema": "http://json-schema.org/draft-07/schema", "type": "object", "definitions": { "position": { "type": "object", "properties": { "line": { "description": "The zero-based line number.", "type": "integer" }, "character": { "description": "The zero-based character number.", "type": "integer" } } }, "range": { "type": "object", "properties": { "start": { "description": "The start of the range.", "$ref": "#/definitions/position" }, "end": { "description": "The end of the range.", "$ref": "#/definitions/position" } } }, "selection": { "type": "object", "properties": { "anchor": { "description": "The starting position of the selection.", "$ref": "#/definitions/position" }, "active": { "description": "The active cursor position of the selection.", "$ref": "#/definitions/position" } } }, "diagnostic": { "type": "object", "properties": { "start": { "description": "The start range of the diagnostic.", "$ref": "#/definitions/position" }, "end": { "description": "The end range of the diagnostic.", "$ref": "#/definitions/position" }, "message": { "description": "The diagnostic message.", "type": "string" }, "severity": { "description": "The diagnostic severity.", "type": "integer" }, "relatedInformation": { "description": "The diagnostic code.", "type": "integer" } } }, "activeTextEditor": { "type": "object", "properties": { "selections": { "description": "Selections in the active editor.", "type": "array", "items": { "$ref": "#/definitions/selection" } }, "documentFilePath": { "description": "A relative filepath to the active file test fixture.", "type": "string" }, "visibleRanges": { "description": "Visible ranges in the active editor.", "type": "array", "items": { "$ref": "#/definitions/range" } }, "languageId": { "description": "The language ID of the active editor.", "type": "string" } } } }, "required": [ "activeTextEditor", "terminalBuffer", "debugConsoleOutput", "activeFileDiagnostics" ], "properties": { "activeTextEditor": { "description": "The active editor.", "$ref": "#/definitions/activeTextEditor" }, "terminalBuffer": { "description": "The contents of the terminal buffer.", "type": "string" }, "debugConsoleOutput": { "description": "The contents of the debug console.", "type": "string" }, "activeFileDiagnostics": { "description": "The diagnostics for the active file.", "type": "array", "items": { "$ref": "#/definitions/diagnostic" } }, "workspaceFoldersFilePaths": { "description": "A list of workspace folder paths.", "type": "array", "items": { "type": "string" } }, "symbols": { "description": "Symbols in the active file.", "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "kind": { "type": "integer" }, "containerName": { "type": "string" }, "filePath": { "type": "string" }, "start": { "$ref": "#/definitions/position" }, "end": { "$ref": "#/definitions/position" } } } } } } ================================================ FILE: .vscode/tasks.json ================================================ { "version": "2.0.0", "tasks": [ { "label": "Install dependencies", "type": "shell", "command": "npm ci", "inSessions": true, "runOptions": { "runOn": "worktreeCreated" } }, { "label": "Compile & Launch Extension Host", "type": "shell", "command": "npm run compile && COPILOT_LOG_TELEMETRY=true VSCODE_DEV_DEBUG=1 code-insiders --extensionDevelopmentPath=$(pwd)", "inSessions": true }, { "label": "Typecheck", "type": "shell", "command": "npm run typecheck", "inSessions": true }, { "label": "compile", "type": "npm", "script": "compile", "problemMatcher": "$esbuild", }, { "type": "npm", "script": "watch", "label": "npm: watch - DO NOT USE", // The auto-discovered npm task is also called watch, so we change its name here. This is here until people clear their histories. }, { "label": "ensure-deps", "type": "shell", "windows": { "command": "if not exist node_modules npm ci", "options": { "shell": { "executable": "cmd.exe", "args": ["/c"] } } }, "linux": { "command": "test -d node_modules || npm ci" }, "osx": { "command": "test -d node_modules || npm ci" }, "group": "build", "presentation": { "reveal": "never", "close": true }, "problemMatcher": [], "isBackground": true, "promptOnClose": false }, { "label": "start-watch-tasks", "dependsOn": [ "npm: watch:tsc-extension", "npm: watch:tsc-extension-web", "npm: watch:tsc-simulation-workbench", "npm: watch:esbuild" ], "dependsOrder": "parallel", "group": "build", "presentation": { "reveal": "never" } }, { "label": "watch", "dependsOn": [ "ensure-deps", "start-watch-tasks", ], "dependsOrder": "sequence", "presentation": { "reveal": "never", }, "group": { "kind": "build", "isDefault": true }, "runOptions": { "runOn": "folderOpen" } }, { "type": "npm", "script": "watch:tsc-extension", "group": "build", "problemMatcher": "$tsc-watch", "isBackground": true, "label": "npm: watch:tsc-extension", "presentation": { "group": "watch", "reveal": "never" } }, { "type": "npm", "script": "watch:tsc-extension-web", "group": "build", "problemMatcher": "$tsc-watch", "isBackground": true, "label": "npm: watch:tsc-extension-web", "presentation": { "group": "watch", "reveal": "never" } }, { "type": "npm", "script": "watch:tsc-simulation-workbench", "group": "build", "problemMatcher": "$tsc-watch", "isBackground": true, "label": "npm: watch:tsc-simulation-workbench", "presentation": { "group": "watch", "reveal": "never" } }, { "type": "npm", "script": "watch:esbuild", "group": "build", "problemMatcher": "$esbuild-watch", "isBackground": true, "label": "npm: watch:esbuild", "presentation": { "group": "watch", "reveal": "never" } }, { "label": "simulate", "type": "process", "command": "${workspaceFolder}/script/simulate.sh", "windows": { "command": "powershell", "args": ["-ExecutionPolicy", "Bypass", "-File", "${workspaceFolder}/script/simulate.ps1"] }, "args": [], "group": { "kind": "test", "isDefault": true }, "presentation": { "reveal": "always", "panel": "dedicated", "clear": true, "focus": true }, "problemMatcher": [] } ] } ================================================ FILE: .vscode-test.mjs ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { defineConfig } from '@vscode/test-cli'; import { readFileSync, writeFileSync } from 'fs'; import { dirname, resolve } from 'path'; import { loadEnvFile } from 'process'; import { fileURLToPath } from 'url'; const isSanity = process.argv.includes('--sanity'); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); if (isSanity) { loadEnvFile(resolve(__dirname, '.env')); } const packageJsonPath = resolve(__dirname, 'package.json'); const raw = readFileSync(packageJsonPath, 'utf8'); const pkg = JSON.parse(raw); pkg.engines.vscode = pkg.engines.vscode.split('-')[0]; // remove the date from the vscode engine version writeFileSync(packageJsonPath, JSON.stringify(pkg, null, '\t')); // and revert it once done process.on('exit', () => writeFileSync(packageJsonPath, raw)); const isRecoveryBuild = !pkg.version.endsWith('.0'); export default defineConfig({ files: __dirname + (isSanity ? '/dist/sanity-test-extension.js' : '/dist/test-extension.js'), version: isRecoveryBuild ? 'stable' : 'insiders-unreleased', launchArgs: [ '--disable-extensions', '--profile-temp' ], mocha: { ui: 'tdd', color: true, forbidOnly: !!process.env.CI, timeout: 5000 } }); ================================================ FILE: .vscodeignore ================================================ ** !assets/ assets/walkthroughs/** !dist/compiled/** !dist/extension.js !dist/extension.js.LICENSE.txt !dist/*.wasm !dist/*.json !dist/*.bpe !dist/*.tiktoken !dist/node_modules/** !dist/tfidfWorker.js !dist/worker2.js !dist/tikTokenizerWorker.js !dist/diffWorker.js !dist/webview.js !dist/copilotDebugCommand.js !dist/copilotCLIShim.js !dist/cli.js !dist/suggestionsPanelWebview.js !node_modules/@vscode/copilot-typescript-server-plugin/package.json !node_modules/@vscode/copilot-typescript-server-plugin/dist/*.js node_modules/@github/copilot/index.js node_modules/@github/copilot/clipboard/** node_modules/@github/copilot/prebuilds/** node_modules/@github/copilot/sharp/** !node_modules/@github/copilot/package.json !node_modules/@github/copilot/sdk/**/package.json !node_modules/@github/copilot/sdk/*.js !node_modules/@github/copilot/sdk/worker/*.js !node_modules/@github/copilot/sdk/sharp/** !node_modules/@github/copilot/sdk/definitions/*.yaml !CHANGELOG.md !README.md !package.json !package.nls.json !package.nls.*.json !telemetry.json !l10n/bundle.l10n.*.json !ThirdPartyNotices.txt !LICENSE.txt ================================================ FILE: CHANGELOG.md ================================================ ## 0.40 (2026-03-18) GitHub Copilot updates for [VS Code 1.112](https://code.visualstudio.com/updates/v1_112): - Message steering and queueing in Copilot CLI - Preview changes before delegating to Copilot CLI - Clickable file links in Copilot CLI terminal output - Permissions levels in Copilot CLI - Troubleshoot agent behavior with /troubleshoot (Preview) - Export and import agent debug logs (Preview) - Image and binary file support for agents - Automatic symbol references on paste in chat - Customizations discovery in parent repositories - Sandbox locally running MCP servers (Linux and macOS) - Improved UI for MCP Elicitation - Enable or disable plugins and MCP servers - Automatic plugin updates ## 0.39 (2026-03-09) GitHub Copilot updates for [VS Code 1.111](https://code.visualstudio.com/updates/v1_111): - Autopilot and agent permissions - Agent-scoped hooks (Preview) - Debug events snapshot - Chat tip improvements - AI CLI profile group in terminal dropdown (Experimental) ## 0.38 (2026-03-05) GitHub Copilot updates from [February 2026](https://code.visualstudio.com/updates/v1_110): ### Agent controls - **Background agent slash commands** — Chat customization options like prompt files, hooks, and skills are now available in background agent sessions as slash commands. Background agent sessions can also be renamed. - **Claude agent improvements** — Steering and queuing, session renaming, context window rendering with compaction, new slash commands (`/compact`, `/agents`, `/hooks`), `getDiagnostics` tool, and performance improvements for reading sessions. - **Agent Debug panel (Preview)** — New panel showing chat events in real time, including system prompts, tool calls, and customization events. Includes a chart view for visual event hierarchy. Open via **Developer: Open Agent Debug Panel** or the gear icon in the Chat view. - **Auto approve slash commands** — Toggle global auto approve directly from chat input with `/autoApprove` and `/disableAutoApprove` (aliases: `/yolo`, `/disableYolo`). - **Edit mode hidden by default** — Agent mode now handles everything edit mode can do; edit mode is hidden from the agent picker by default, controlled by the `chat.editMode.hidden` setting. Ask mode is now backed by a custom agent definition. - **Ask questions tool improvements** — The `askQuestions` tool moved into VS Code core for improved reliability. You can now send steering messages without dismissing pending questions first. - **Prevent auto-suspend during chat** — VS Code asks the OS not to suspend the machine while a chat request is running. ### Agent extensibility - **Agent plugins (Experimental)** — Prepackaged bundles of chat customizations (skills, commands, agents, MCP servers, hooks) installable from the Extensions view. Configurable plugin marketplaces and local plugin directories. - **Agentic browser tools (Experimental)** — Agents can read and interact with the integrated browser using tools like `openBrowserPage`, `readPage`, `screenshotPage`, `clickElement`, `typeInPage`, and `runPlaywrightCode`. Enable by setting `workbench.browser.enableChatTools` to `true`. - **Create agent customizations from chat** — New `/create-prompt`, `/create-instruction`, `/create-skill`, `/create-agent`, and `/create-hook` slash commands to generate customization files directly from a conversation. - **Tools for usages and rename** — New `vscode_renameSymbol` tool and updated `usages` tool let agents navigate and refactor code using extension/LSP capabilities with high precision. ### Smarter sessions - **Session memory for plans** — Plans persist to session memory and stay available across conversation turns, surviving compaction. - **Explore subagent for codebase search** — The Plan agent delegates codebase research to a dedicated read-only Explore subagent running on fast models. Configurable via the `chat.exploreAgent.defaultModel` setting. - **Inline chat and chat session integration** — When an agent session already changed a file, inline chat queues new messages into that session instead of making changes in isolation. ### Chat experience - **Redesigned model picker** — New dropdown with Auto, featured/recently used, and other models sections, plus a search box and rich hover details. - **Contextual tips (Experimental)** — Tips in the Chat view help discover features tailored to your usage patterns, controlled by the `chat.tips.enabled` setting. - **Custom thinking phrases** — Customize loading text during reasoning/tool calls via the `chat.agent.thinking.phrases` setting. - **Collapsible terminal tool calls** — Terminal tool invocations displayed as collapsible sections to reduce visual noise, controlled by the `chat.tools.terminal.simpleCollapsible` setting. - **OS notifications for chat** — Configure notifications for chat responses and confirmations to appear even when the window is in focus (`always` option). - **Inline chat hover mode** — New hover-based UI for inline chat (set `inlineChat.renderMode` to `hover`). - **Inline chat affordance** — Selection-triggered affordance for starting inline chat in the editor or gutter, controlled by the `inlineChat.affordance` setting. ### Code editing - **Long-distance next edit suggestions** — NES now predicts and suggests edits anywhere in the file, not just near the cursor. - **NES eagerness** — New eagerness option in the Copilot Status Bar to control suggestion frequency vs. relevance. --- ## 0.37 (2026-02-04) GitHub Copilot updates from [January 2026](https://code.visualstudio.com/updates/v1_109): ### Chat UX - **Message steering and queueing (Experimental)** — Send follow-up messages while a request is running: queue messages, steer the agent mid-task, or stop and send a new message. Drag-and-drop reordering for queued messages. - **Anthropic models now show thinking tokens** — Claude models surface thinking tokens with configurable display styles, interleaved tool calls, auto-expanding failed tool calls, scrollable thinking content, and shimmer animations. - **Mermaid diagrams in chat responses** — Interactive Mermaid diagrams (flowcharts, sequence diagrams, etc.) rendered directly in chat with pan, zoom, and open-in-editor support. - **Ask Questions tool (Experimental)** — Agent can ask clarifying questions with single/multi-select options, free text input, and recommended answers highlighted. - **Plan agent improvements** — Structured 4-phase workflow (Discovery → Alignment → Design → Refinement). Invokable via `/plan` slash command. - **Context window details** — New indicator in chat input showing token usage breakdown by category. - **Inline chat UX revamp (Preview)** — New text-selection affordance and contextual rendering for triggering inline chat. - **Model descriptions in the model picker** — Hover or keyboard focus shows model details at a glance. - **Terminal command output improvements** — Syntax highlighting for inline Node/Python/Ruby, working directory display, command intent descriptions, output streaming for long-running commands, interactive input in embedded terminals. - **Delete all hidden terminals** — One-click delete for all hidden chat terminals. ### Agent session management - **Session type picker** — New picker to choose agent type (local, background, cloud) or hand off ongoing sessions between environments. - **Agent Sessions view improvements** — Resizable side-by-side sessions, multi-select bulk operations, improved stacked view with filters. - **Agent status indicator** — Command center indicator showing in-progress, unread, and attention-needed sessions. - **Parallel subagents** — Subagents can now run in parallel for faster task completion. - **Search subagent (Experimental)** — Dedicated search subagent with isolated context window for iterative codebase searches. - **Cloud agent improvements** — Model selection, third-party coding agents (Claude, Codex), custom agents, multi-root workspace support, checkout without GitHub PR extension. - **Background agent improvements** — Custom agents, image attachments, multi-root workspace support, auto-commit at end of each turn. - **Agent sessions welcome page (Experimental)** — New startup editor showing recent agent sessions with quick actions and embedded chat. ### Agent customization - **Agent hooks (Preview)** — Execute custom shell commands at agent lifecycle points (PreToolUse, PostToolUse, SessionStart, Stop, etc.). Compatible with Claude Code and Copilot CLI hook formats. - **Skills as slash commands** — Agent Skills invokable via `/` in chat alongside prompt files. Controllable via `user-invokable` and `disable-model-invocation` frontmatter. - **`/init` command** — Generate or update workspace instructions (`copilot-instructions.md`, `AGENTS.md`) based on codebase analysis. - **Agent Skills generally available** — Enabled by default. Manageable via Commands. Configurable skill locations. Extension authors can distribute skills via `chatSkills` contribution point. - **Organization-wide instructions** — GitHub organization custom instructions automatically applied to chat sessions. - **Custom agent file locations** — Configurable directories for agent definitions via `chat.agentFilesLocations`. - **Control agent invocation** — New frontmatter: `user-invokable`, `disable-model-invocation`, `agents` (limit subagent access). - **Multiple model support for custom agents** — Specify fallback model lists in frontmatter. - **Chat customization diagnostics** — New diagnostics view showing all loaded agents, prompts, instructions, and skills with error details. - **Language Models editor improvements** — Multiple configurations per provider, Azure model configuration, provider group management, keyboard access, `chatLanguageModels.json` config file, model provider configuration UI. - **Language model configuration** — Default model for plan implementation, default model for inline chat, model parameter for agent handoffs. - **Agent customization skill (Experimental)** — Built-in skill that teaches the agent how to create custom agents, instructions, prompts, and skills. ### Agent extensibility - **Claude compatibility** — VS Code reads Claude configuration files directly: `CLAUDE.md` instructions, `.claude/agents`, `.claude/skills`, `.claude/settings.json` hooks. - **Agent orchestration** — Building blocks for multi-agent workflows using custom agents, subagents, and invocation controls. Community examples: Copilot Orchestra, GitHub Copilot Atlas. - **Claude Agent (Preview)** — Delegate tasks to Claude Agent SDK using Copilot subscription models. Uses official Anthropic agent harness. - **Anthropic model improvements** — Messages API with interleaved thinking, tool search tool, context editing (Experimental). - **MCP Apps support** — MCP servers can display rich, interactive UI in chat responses. - **Custom registry base URLs for MCP packages** — Support for private/alternative package registries. ### Agent optimizations - **Copilot Memory (Preview)** — Store and recall context across sessions. Agent auto-saves and retrieves relevant memories. - **External indexing for non-GitHub workspaces (Preview)** — Remote indexing for semantic code search in non-GitHub repositories. - **Read files outside workspace** — Agents can read external files/directories with user permission. - **Performance improvements** — Faster large chat scrolling/persistence, parallel dependent task processing. ### Agent security and trust - **Terminal sandboxing (Experimental)** — Restrict file system access to workspace folder and network access to trusted domains (macOS/Linux only). - **Terminal tool lifecycle improvements** — Manual background push, required timeout property, `awaitTerminal` and `killTerminal` tools. - **Terminal auto-approval expansions** — New safe commands auto-approved: `Set-Location`, `dir`, `od`, `xxd`, `docker`, `npm`/`yarn`/`pnpm` safe sub-commands. ### Code editing (AI-related) - **Rename suggestions for TypeScript** — Also works when typing over existing declarations. - **Improved ghost text visibility** — Dotted underline for short inline suggestions (fewer than 3 characters). - **Copilot extension deprecated** — GitHub Copilot extension fully deprecated; all functionality in GitHub Copilot Chat extension. ### Enterprise - **Improved GitHub organization policy enforcement** — Policies correctly apply based on preferred Copilot account; enforced during network unavailability at startup. --- ## 0.36 (2026-01-08) GitHub Copilot updates from [December 2025](https://code.visualstudio.com/updates/v1_108): ### Agents - **Agent Skills (Experimental)** — New capability to teach the coding agent domain-specific knowledge via skill folders containing `SKILL.md` files. Auto-detected from `.github/skills` (or `.claude/skills/`), loaded on-demand into chat context. - **Agent Sessions view improvements** — Keyboard access, state-based session grouping, changed files and PR info per session, multi-select archiving, and accessibility improvements. ### Chat - **Chat picker based on agent sessions** — Quick Pick for chat sessions now mirrors the Agent Sessions view with actions like archive, rename, and delete. - **Chat title improvements** — Title control now visible regardless of Activity Bar configuration; select the title to jump between sessions. - **Open empty Chat on restart** — Previous sessions no longer auto-restored on restart; configurable via `chat.restoreLastPanelSession`. - **Terminal tool auto approve defaults** — New safe commands auto-approved by default (e.g., `git ls-files`, `rg`, `sed`, `Out-String`). Workspace npm scripts auto-approved when in `package.json`. Informational messages when rules deny auto-approval. - **Session and workspace rules for terminal commands** — Allow dropdown now supports allowing commands for the current session or workspace scope. - **Terminal tool prevents adding to shell history** — Commands run by the terminal tool excluded from shell history (bash, zsh, pwsh, fish). - **Streaming chat responses in Accessible View** — Chat responses now stream dynamically in the Accessible View without needing to close and reopen. - **MCP server output excluded from Accessible View** — Reduces noise by excluding MCP server output from the Accessible View. --- ## 0.35 (2025-12-10) GitHub Copilot updates from [November 2025](https://code.visualstudio.com/updates/v1_107): ### Agents - **Agent sessions integrated into Chat view** — Unified experience for managing agent sessions directly in the Chat view (compact, side-by-side, or stacked layouts). Sessions show status, progress, and file change stats. Supports search, filtering, and archiving. - **Local agents remain active when closed** — Local agent sessions continue running in the background when closed, enabling long-running and parallel tasks. - **Continue tasks in background or cloud agents** — Hand off local chat sessions to background or cloud agents via a new "Continue in" option. Context is passed along automatically. - **Isolate background agents with Git worktrees** — Background agents can run in dedicated Git worktrees to avoid file conflicts when running multiple agents simultaneously. - **Adding context to background agents** — Attach selections, problems, symbols, search results, git commits, and more as context to background agent prompts. - **Share custom agents across your GitHub organization (Experimental)** — Define custom agents at the organization level for shared use across teams. - **Custom agents with background agents (Experimental)** — Use custom agents defined in `.github/agents` with background agents. - **Agent tooling reorganization** — Renamed tool references for better compatibility with GitHub custom agents across VS Code and GitHub environments. - **Run agents as subagents (Experimental)** — Custom agents can be used as subagents for delegating subtasks within a chat session, each with its own context window. - **Reuse Claude skills (Experimental)** — VS Code can discover and use Claude Code skills from `~/.claude/skills/` and workspace `.claude/skills/` folders. ### Chat - **Inline chat UX** — Inline chat optimized for single-file code changes; non-code tasks automatically upgrade to the Chat view. - **Language Models editor** — Centralized editor to view, search, filter, and manage language model visibility and providers. Supports adding models from installed providers. - **URL and domain auto approval** — Two-step approval for fetch tool URLs: approve the domain, then review fetched content before use (prompt injection protection). Integrates with Trusted Domains. - **More robust fetch tool** — `#fetch` now handles dynamic/JavaScript-rendered web content (SPAs, Jira, etc.). - **Text search tool can search ignored files** — `#textSearch` can now search files/folders excluded by `.gitignore`, `files.exclude`, or `search.exclude`. - **Rich terminal output in chat** — Terminal output renders in a full `xterm.js` terminal inside chat with preserved output history and ANSI color support. - **Allow all terminal commands in this session** — New option to auto-approve all terminal commands for the current session. - **Keyboard shortcuts for chat terminal actions** — Dedicated keybindings to focus or toggle the most recent chat terminal. - **Keyboard shortcuts for custom agents** — Each custom agent gets a unique command in the Command Palette for keybinding. - **Azure model provider: Entra ID default auth** — Azure BYOK models now default to Entra ID authentication. - **Anthropic models: Extended thinking support** — Configurable thinking budget for Anthropic models (default: 4,000 tokens). Supports interleaved thinking via BYOK. - **Chat view appearance improvements** — New chat title control, optional welcome banner, and restore previous session on reopen. - **Diffs for edits to sensitive files** — Proposed changes to sensitive files (e.g., `settings.json`) now shown as diffs for easier review. - **Collapsible reasoning and tools output (Experimental)** — Successive tool calls collapsed by default with AI-generated summaries to reduce visual noise. ### Code editing (AI-related) - **Rename suggestions for TypeScript** — AI predicts symbol renames and suggests related renames across the file. - **New model for next edit suggestions** — Improved model with better acceptance/dismissal performance. - **Preview next edit suggestions outside the viewport** — Suggestions outside the viewport show a preview at the cursor position. - **Copilot extensions unification** — Inline suggestions fully served from Copilot Chat extension; GitHub Copilot extension disabled by default. Full deprecation planned for January 2026. ### MCP - **Support for latest MCP specification (2025-11-25)** — Adds URL mode elicitation, tasks for long-running tool calls, and enhanced enum choices. - **GitHub MCP Server provided by Copilot Chat (Preview)** — Built-in GitHub MCP server with automatic authentication, configurable toolsets, and read-only mode. ### Enterprise - **Control auto approval for agent tools** — New setting to define which tools are eligible for auto-approval; enforceable via enterprise policy. - **Disable agents by policy** — Agent picker communicates when agents are unavailable due to enterprise policy. - **GitHub Enterprise policies in Codespaces** — Enterprise/org policies (e.g., MCP registry) now apply in GitHub Codespaces. --- ## 0.33 (2025-11-12) GitHub Copilot updates from [October 2025](https://code.visualstudio.com/updates/v1_106): ### Agents - **Agent Sessions view** — Centralized view for managing all active chat sessions (local and cloud), including Copilot coding agent, Copilot CLI, and OpenAI Codex. Supports search and a consolidated single-view mode. - **Plan agent** — New agent that breaks down complex tasks into step-by-step implementation plans before writing code. Supports clarifying questions and iterative refinement. Can be customized per team. - **Cloud agents** — Copilot coding agent integration moved from GitHub PR extension into Copilot Chat extension. Deeper integration with GitHub Mission Control for seamless transitions. - **CLI agents** — Initial integration with Copilot CLI, allowing new/resumed CLI agent sessions in chat editors or integrated terminal. - **Agent delegation** — Improved cloud delegation from chat panel and CLI (via `/delegate` command). - **Chat modes renamed to custom agents** — `.chatmode.md` → `.agents.md` files in `.github/agents`. New metadata properties: `target`, `name`, `argument-hint`, `handoffs`. ### Chat - **Embeddings-based tool selection** — Improved tool filtering for users with 100+ tools; faster and more accurate tool selection. - **Tool approvals and trust** — Post-approval for external data (prompt injection protection), trust all tools from a server/extension at once, updated tool approval management. - **Terminal tool improvements** — Tree-sitter-based parser for better subcommand detection, file write/redirection detection, shell-specific prompts, PowerShell `&&` rewriting, attach terminal commands as chat context, inline terminal output in chat, hidden chat terminal discovery. - **Save conversation as prompt** — `/savePrompt` command to save chat conversations as reusable `.prompt` files. - **Edit welcome prompts** — Right-click prompts in Chat welcome view to edit the underlying prompt file. - **Auto-open edited files disabled by default** — Agent no longer auto-opens edited files (configurable). - **Reasoning (Experimental)** — Thinking tokens now supported in GPT-5-Codex, GPT-5, GPT-5 mini, and Gemini 2.5 Pro. New display styles and collapsible tool calls in thinking UI. - **Inline chat v2 (Preview)** — Modernized single-prompt, single-file inline chat for code changes only. - **Chat view UX improvements** — New chat dropdown, reorganized tools/MCP server actions, copy math source support. ### Code editing (AI-related) - **Inline suggestions open-sourced** — Merged into vscode-copilot-chat repo; Copilot and Copilot Chat extensions consolidating into one. GitHub Copilot extension to be deprecated by early 2026. - **Snooze inline suggestions** — Pause suggestions directly from the gutter icon with a configurable duration. ### MCP - **Organization MCP registry** — Custom MCP registry via GitHub org policies to control which MCP servers can be installed/started. - **Install MCP servers to workspace** — Add MCP servers to `.vscode/mcp.json` for team sharing. - **Client ID Metadata Document auth** — New OAuth flow for remote MCP servers (more secure than DCR). - **WWW-Authenticate scope step up** — Dynamic scope escalation for remote MCP servers (least-privilege principle). ### Language-specific AI features - **Python: Copilot Hover Summaries as docstring** — Insert AI-generated summaries directly as docstrings. - **Python: Localized Copilot Hover Summaries** — Respects VS Code display language. ### Preview - **Language Models editor** — Centralized editor for viewing, searching, filtering, and managing model visibility in the chat model picker. Add models from installed providers (Insiders only). --- ## 0.32 (2025-10-09) GitHub Copilot updates from [September 2025](https://code.visualstudio.com/updates/v1_105): ### Chat #### Fully qualified tool names Prompt files and custom chat modes enable you to specify which tools can be used. To avoid naming conflicts between built-in tools and tools provided by MCP servers or extensions, we now support fully qualified tool names for prompt files and chat modes. This also helps with discovering missing extensions or MCP servers. Tool names are now qualified by the MCP server, extension, or tool set they are part of. For example, instead of `codebase`, you would use `search/codebase` or `list_issues` would be `github/github-mcp-server/list_issues`. You can still use the previous notation, however a code actions helps migrating to the new names. ![Screenshot of a prompt file showing a Code Action to update an unqualified tool name.](https://code.visualstudio.com/assets/updates/1_105/qualified_tool_names.png) #### Improved edit tools for bring-your-own-key models **Setting**: `github.copilot.chat.customOAIModels` To make working with custom models better integrated with VS Code built-in tools, we improved the set of edit tools given to [Bring Your Own Key (BYOK)](https://code.visualstudio.com/docs/copilot/customization/language-models#_bring-your-own-language-model-key) custom models. In addition, we enhanced our default tools and added a 'learning' mechanism to select the optimal tool set for custom models. If you're [using OpenAI-compatible models](https://code.visualstudio.com/docs/copilot/customization/language-models#_use-an-openaicompatible-model), you can also explicitly configure the list of edit tools with the `github.copilot.chat.customOAIModels` setting. #### Chat user experience improvements ##### OS notifications for chat responses **Setting**: `chat.notifyWindowOnResponseReceived` In VS Code 1.103, we introduced OS notifications for chat sessions that required a user confirmation when the VS Code window was not focused. In this release, we are expanding this functionality to show an OS badge and notification toast when a chat response is received. The notification includes a preview of the response, and selecting it brings focus to the chat input. ![Screenshot showing an OS notification while the VS Code window is unfocused.](https://code.visualstudio.com/assets/updates/1_105/chat-notification.png) You can control the notification behavior with the `chat.notifyWindowOnResponseReceived` setting. ##### Chain of thought (Experimental) **Setting**: `chat.agent.thinkingStyle` Chain of thought shows the model’s reasoning as it responds, which can be great for debugging or understanding suggestions the model provides. With the introduction of GPT-5-Codex, thinking tokens are now shown in chat as expandable sections in the response. ![Screenshot of a chat response showing thinking tokens as expandable sections in the response.](https://code.visualstudio.com/assets/updates/1_105/chat-thinking-tokens.png) You can configure how to display or hide chain of thought with the `chat.agent.thinkingStyle` setting. Thinking tokens will soon be available in more models as well! ##### Show recent chat sessions (Experimental) **Setting**: `chat.emptyState.history.enabled` Last milestone, we introduced [prompt file suggestions](https://code.visualstudio.com/updates/v1_104#_configure-prompt-file-suggestions-experimental) to help you get started when creating a new chat session (Ctrl+L or Cmd+L on macOS). In this release, we are building on that by showing your recent local chat conversations. This helps you quickly pick up where you left off or revisit past conversations. ![Screenshot of the Chat view showing recent local chat conversations when there are no active chat sessions.](https://code.visualstudio.com/assets/updates/1_105/chat-history-on-empty.png) By default, this functionality is off, but you can enable it with the `chat.emptyState.history.enabled` setting. ##### Keep or undo changes during an agent loop Previously, when an agent was still processing your chat request, you could not keep or undo file edits until the agent finished. Now, you can keep or undo changes to files while an edit loop is happening. This enables you to have more control, especially for long-running tasks. ##### Keyboard shortcuts for navigating user chat messages To quickly navigate through your previous chat prompts in the chat session, we added keyboard shortcuts for navigating up and down through your chat messages: * Navigate previous: Ctrl+Alt+Up or Cmd+Option+Up on macOS * Navigate next: Ctrl+Alt+Down or Cmd+Option+Down on macOS #### Agent sessions This milestone, we made several improvements to the Chat Sessions view and the experience of delegating tasks to remote coding agents: ##### Chat Sessions view enhancements **Setting**: `chat.agentSessionsViewLocation` The [Chat Sessions view](https://code.visualstudio.com/docs/copilot/copilot-coding-agent#_manage-sessions-with-dedicated-chat-editor-experimental) provides a centralized location for managing both local chat conversations and remote coding agent sessions. This view enables you to work with multiple AI sessions simultaneously, track their progress, and manage long-running tasks efficiently. In this release, we made several UI refinements and performance improvements to enhance the Chat Sessions experience. * The Chat Sessions view continues to support features like Status Bar tracking for monitoring multiple coding agents, context menus for session management, and rich descriptions to provide detailed context for each session. * Quickly initiate a new session by using the "+" button in the view header. ![Screenshot of the Chat Sessions view with a new session open via the + button.](https://code.visualstudio.com/assets/updates/1_105/chat-sessions.png) #### Delegating to remote coding agents A typical scenario for working with remote coding agents is to first discuss and plan a task in a local chat session, where you have access to the full context of your codebase, and then delegate the implementation work to a remote coding agent. The remote agent can then work on the task in the background and create a pull request with the solution. If you're working in a repository that has [Copilot coding agent enabled](https://aka.ms/coding-agent-docs), the **Delegate to coding agent** button in the Chat view now appears by default. ![Screenshot of the Chat view with the Delegate to coding agent button highlighted.](https://code.visualstudio.com/assets/updates/1_105/delegate-button.png) When you use the delegate action, all the context from your chat conversation, including file references, are forwarded to the coding agent. If your conversation exceeds the coding agent's context window, VS Code automatically summarizes and condenses the information to fit the window. #### Terminal commands ##### Autoreply to prompts (Experimental) **Setting**: `chat.tools.terminal.autoReplyToPrompts` We introduced an opt-in setting, `chat.tools.terminal.autoReplyToPrompts`, which enables the agent to respond to prompts for input in the terminal automatically, like `Confirm? y/n`. ##### Free form input request detection When the terminal requires free-form input, we now display a confirmation prompt. This lets you stay focused on your current work and only shift attention when input is needed. #### Model availability This milestone, we added support for the following models in chat. The available models depend on your Copilot plan and configuration. * **GPT-5-Codex**, OpenAI’s GPT-5 model, optimized for agentic coding. * **Claude Sonnet 4.5**, Anthropic’s most advanced model for coding and real-world agents. You can choose between different models with the model picker in chat. Learn more about [language models in VS Code](https://code.visualstudio.com/docs/copilot/customization/language-models). ### MCP #### MCP marketplace (Preview) **Setting**: `chat.mcp.gallery.enabled` VS Code now includes a built-in MCP marketplace that enables users to browse and install MCP servers directly from the Extensions view. This is powered by the [GitHub MCP registry](https://github.com/mcp) and provides a seamless experience for discovering and managing MCP servers directly within the editor. > **Note**: This feature is currently in preview. Not all features are available yet and the experience might still have some rough edges. The MCP marketplace is disabled by default. When no MCP servers are installed, you see a welcome view in the Extensions view that provides easy access to enable the marketplace. You can also enable the MCP marketplace manually using the setting `chat.mcp.gallery.enabled`. ![Screenshot showing the MCP Servers welcome view with text describing how to browse and install Model Context Protocol servers, and an "Enable MCP Servers Marketplace" button.](https://code.visualstudio.com/assets/updates/1_105/mcp-servers-welcome.png) To browse the MCP servers from the Extensions view: * Use the `@mcp` filter in the Extensions view search box * Select **MCP Servers** from the filter dropdown in the Extensions view * Search for specific MCP servers by name ![Screenshot showing the GitHub MCP server details from the MCP server marketplace inside VS Code.](https://code.visualstudio.com/assets/updates/1_105/mcp-server-editor.png) #### Autostart MCP servers **Setting**: `chat.mcp.autostart` In this release, new or outdated MCP servers are now started automatically when you send a chat message. VS Code also avoids triggering interactions such as dialogs when autostarting a server, and instead adds an indicator in chat to let you know that a server needs attention. ![Screenshot of the Chat view, showing a notification message that the GitHub MCP requires restarting.](https://code.visualstudio.com/assets/updates/1_105/mcp_autostart_prompt.png) With MCP autostart on by default, we no longer eagerly activate extensions and instead only activate MCP-providing extensions when the first chat message is sent. For extension developers, we also added support for the `when` clause on the `mcpServerDefinitionProviders` contribution point, so you can avoid activation when it's not relevant. #### Improved representation of MCP resources returned from tools Previously, our implementation of tool results that contain resources left it up to the model to retrieve those resources, without clear instructions on how to do so. In this version of VS Code, by default, we include a preview of the resource content and add instructions to retrieve the complete contents. This should lead to better model performance when using such tools. #### MCP specification updates This milestone, we adopted the following updates to the MCP specification: * [SEP-973](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/955), which lets MCP servers specify `icons` to associate with their data. This can be used to give a custom icon to servers, resources, and tools. ![Screenshot of the tools picker, showing one of the MCP servers in the list with a custom icon.](https://code.visualstudio.com/assets/updates/1_105/mcp_icons.png) HTTP MCP servers must provide icons from the same authority that the MCP server itself is listening on, while stdio servers are allowed to reference `file:///` URIs on disk. * [SEP-1034](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1035), which lets MCP servers provide `default` values when using elicitation. ### Accessibility #### Chat improvements **Setting**: `accessibility.verboseChatProgressUpdates` A new setting, `accessibility.verboseChatProgressUpdates`, enables more detailed announcements for screen reader users about chat activity. From the chat input, users can focus the last focused chat response item with Ctrl+Shift+Up. ### Source Control #### Resolve merge conflicts with AI When opening a file with git merge conflict markers, you are now able to resolve merge conflicts with AI. We added a new action in the lower right hand corner of the editor. Selecting this new action opens the Chat view and starts an agentic flow with the merge base and changes from each branch as context. ![Screenshot of the proposed merge conflict resolution in the editor.](https://code.visualstudio.com/assets/updates/1_105/merge-conflict-resolution.png) You can review the proposed merge conflict resolution in the editor and follow up with additional context if needed. You can customize the merge conflict resolution by using an `AGENTS.md` file. #### Add history item change to chat context A couple of milestones ago, we added the capability to view the files in each history item shown in the Source Control Graph view. You can now add a file from a history item as context to a chat request. This can be useful when you want to provide the contents of a specific version of a file as context to your chat prompt. To add a file from a history item to chat, select a history item to view the list of files, right-click on a particular file, and then select **Add to Chat** from the context menu. ### Testing #### Run tests with code coverage If you have a testing extension installed for your code, the `runTests` tool in chat enables the agent to run tests in your codebase by using the [VS Code testing integration](https://code.visualstudio.com/docs/debugtest/testing) rather than running them from the command line. In this release, the `runTests` tool now also reports test code coverage to the agent. This enables the agent to generate and verify tests that cover the entirety of your code. --- ## 0.31 (2025-09-11) GitHub Copilot updates from [August 2025](https://code.visualstudio.com/updates/v1_104): ### Chat #### Auto model selection (Preview) This iteration, we're introducing auto model selection in chat. When you choose the **Auto** model in the model picker, VS Code automatically selects a model to ensure that you get the optimal performance and avoid rate limits. Auto model selection is currently in preview and we are rolling it out to all GitHub Copilot users in VS Code in the following weeks, starting with the individual Copilot plans. ![Screenshot that shows the model picker in the Chat view, showing the Auto option.](https://code.visualstudio.com/assets/updates/1_104/model-dropdown-auto.png) Auto will choose between Claude Sonnet 4, GPT-5, GPT-5 mini, and GPT-4.1 and Gemini Pro 2.5, unless your organization has disabled access to these models. When using auto model selection, VS Code uses a variable model multiplier, based on the selected model. If you are a paid user, auto will apply a 10% request discount. You can view the selected model and the model multiplier by hovering over the response in the Chat view. ![Screenshot of a chat response, showing the selected model on hover.](https://code.visualstudio.com/assets/updates/1_104/auto-model-multiplier.png) Learn more about [auto model selection in VS Code](https://code.visualstudio.com/docs/copilot/customization/language-models). #### Confirm edits to sensitive files **Setting**: `chat.tools.edits.autoApprove` In agent mode, the agent can autonomously make edits to files in your workspace. This might include accidentally or maliciously modifying or deleting important files such as configuration files, which could cause immediate negative side-effects on your machine. Learn more about [security considerations when using AI-powered development tools](https://code.visualstudio.com/docs/copilot/security). In this release, the agent now explicitly asks for user confirmation before making edits to certain files. This provides an additional layer of safety when using agent mode. With the `chat.tools.edits.autoApprove` setting, you can configure file patterns to indicate which files require confirmation. Common system folders, dotfiles, and files outside your workspace will require confirmation by default. ![Screenshot showing the confirmation dialog for sensitive file edits in the Chat view.](https://code.visualstudio.com/assets/updates/1_104/chat-edit-sensitive-file.png) #### Support for AGENTS.md files (Experimental) **Setting**: `chat.useAgentsMdFile` An `AGENTS.md` file lets you provide context and instructions to the agent. Starting from this release, when you have an `AGENTS.md` file in your workspace root(s), it is automatically picked up as context for chat requests. This can be useful for teams that use multiple AI agents. Support for `AGENTS.md` files is enabled by default and can be controlled with the `chat.useAgentsMdFile` setting. See for more information about `AGENTS.md` files. Learn more about [customizing chat in VS Code](https://code.visualstudio.com/docs/copilot/customization/overview) to your practices and team workflows. #### Improved changed files experience This iteration, the changed files list has been reworked with several quality-of-life features. These changes should improve your experience when working in agent mode! * The list of changed files is now collapsed by default to give more space to the chat conversation. While collapsed, you can still see the files changed count and the lines added or removed. * When you keep or accept a suggested change, the file is removed from the files changed list. * When you stage or commit a file using the Source Control view, this automatically accepts the proposed file changes. * Changes _per file_ (lines added or removed) are now shown for each item in the list. #### Use custom chat modes in prompt files Prompt files are Markdown files in which you write reusable chat prompts. To run a prompt file, type `/` followed by the prompt file name in the chat input field, or use the Play button when you have the prompt file open in the editor. You can specify which chat mode should be used for running the prompt file. Previously, you could only use built-in chat modes like `agent`, `edit`, or `ask` in your prompt files. Now, you can also reference custom chat modes in your prompt files. ![Screenshot showing IntelliSense for custom chat modes in prompt files.](https://code.visualstudio.com/assets/updates/1_104/custom_modes_in_prompt_files.png) Learn more about [customizing chat in VS Code](https://code.visualstudio.com/docs/copilot/customization/overview) with prompt files, chat modes, and custom instructions. #### Configure prompt file suggestions (Experimental) **Setting**: `chat.promptFilesRecommendations` Teams often create custom prompt files to standardize AI workflows, but these prompts can be hard to discover when users need them most. You can now configure which prompt files appear as suggestions in the Chat welcome view based on contextual conditions. The new `chat.promptFilesRecommendations` setting supports both simple boolean values and when-clause expressions for context-aware suggestions. ```jsonc { "chat.promptFilesRecommendations": { "plan": true, // Always suggest "a11y-audit": "resourceExtname == .html", // Only for HTML files "document": "resourceLangId == markdown", // Only for Markdown files "debug": false // Never suggest } } ``` This helps teams surface the right AI workflows at the right time, making custom prompts more discoverable and relevant to your workspace and file type. #### Select tools in tool sets [Tool sets](https://code.visualstudio.com/docs/copilot/chat/chat-agent-mode#_define-tool-sets) are a convenient way to group related tools together and VS Code has several built-in tool sets like `edit` or `search`. The tools picker now shows which tools are part of each tool set and you can individually enable or disable each tool. You can access the tools picker via the `Configure Tools...` button in the Chat view. ![Screenshot showing the tools picker with an expanded edit tool set, listing all available tools.](https://code.visualstudio.com/assets/updates/1_104/tools_in_toolsets.png) #### Configure font used in chat **Settings**: `chat.fontFamily`, `chat.fontSize` VS Code lets you choose which font to use across the editor, however the Chat view lacked that configurability. We have now added two new settings for configuring the font family (`chat.fontFamily`) and font size (`chat.fontSize`) of chat messages. ![Screenshot showing the Chat view with a custom font and font size.](https://code.visualstudio.com/assets/updates/1_104/chat-configure-font.png) > **Note**: content for lists currently does not yet honor these settings, but this is something that we are working on fixing in the upcoming releases. #### Collaborate with coding agents (Experimental) With coding agents, you delegate tasks to AI agents to be worked on in the background. You can have multiple such agents work in parallel. We're continuing to evolve the chat sessions experience to help you collaborate more effectively with coding agents. ##### Chat Sessions view **Setting**: `chat.agentSessionsViewLocation` The Chat Sessions view provides a single, unified view for managing both local and contributed chat sessions. We've significantly enhanced the Chat Sessions view where you can now perform all key operations, making it easier to iterate and finalize your coding tasks. * **Status Bar tracking**: Monitor progress across multiple coding agents directly from the Status Bar. * **Multi-session support**: Launch and manage multiple chat sessions from the same view. * **Expanded context menus**: Access more actions to interact with your coding agents efficiently. * **Rich descriptions**: With rich description enabled, each list entry now includes detailed context to help you quickly find relevant information. ##### GitHub coding agent integration We've improved the integration of [GitHub coding agents](https://code.visualstudio.com/docs/copilot/copilot-coding-agent) with chat sessions to deliver a smoother, more intuitive experience. * **Chat editor actions**: Easily view or apply code changes, and check out pull requests directly from the chat editor. * **Seamless transitions**: Move from local chats to GitHub agent tasks with improved continuity. * **Better session rendering**: Various improvements on cards and tools rendering for better visual clarity. * **Performance boosts**: Faster session loading for a more responsive experience. ##### Delegate to coding agent We continued to expand on ways to delegate local tasks in VS Code to a Copilot coding agent: * Fix todos with coding agent: Comments starting with `TODO` now show a Code Action to quickly initiate a coding agent session. ![Screenshot of a code action above a TODO comment called Delegate to coding agent.](https://code.visualstudio.com/assets/updates/1_104/coding-agent-todo.png) * Delegate from chat (`githubPullRequests.codingAgent.uiIntegration`): Additional context, including file references, are now forwarded to GitHub coding agent when you perform the **Delegate to coding agent** action in chat. This enables you to precisely plan out a task before handing it off to coding agent to complete it. A new chat editor is opened with the coding agent's progress shown in real-time. _Theme: [Sharp Solarized](https://marketplace.visualstudio.com/items?itemName=joshspicer.sharp-solarized) (preview on [vscode.dev](https://vscode.dev/editor/theme/joshspicer.sharp-solarized))_ #### Social sign in with Google The option to sign in or sign up to GitHub Copilot with a Google account is now generally available and rolling out to all users in VS Code. ![Screenshot showing the sign in dialog showing the option to use a Google account.](https://code.visualstudio.com/assets/updates/1_104/google.png) You can find more information about this in the [announcement GitHub blog post](https://github.blog/changelog/2025-07-15-social-login-with-google-is-now-generally-available). #### Terminal auto approve **Setting**: `chat.tools.terminal.enableAutoApprove` Automatically approving terminal commands can greatly streamline agent interactions, but it also comes with [security risks](https://code.visualstudio.com/docs/copilot/security). This release introduces several improvements to terminal auto approve to enhance both usability and security. * You can now enable or disable terminal auto approve with the `chat.tools.terminal.enableAutoApprove` setting. This setting can also be set by organizations via [device management](https://code.visualstudio.com/docs/setup/enterprise#_centrally-manage-vs-code-settings). * Before terminal auto approve is actually enabled, you need to explicitly opt in via a dropdown in the Chat view. ![Screenshot of a terminal command in the Chat view, showing the Enable Auto Approve dropdown.](https://code.visualstudio.com/assets/updates/1_104/terminal-auto-approve-opt-in.png) * From the Chat view, you can conveniently add auto-approve rules for the command being run, or open the configuration setting: ![Screenshot that shows the three standard options are presented for "foo --arg && bar".](https://code.visualstudio.com/assets/updates/1_104/terminal-auto-approve-ui.png) _Theme: [Sapphire](https://marketplace.visualstudio.com/items?itemName=Tyriar.theme-sapphire) (preview on [vscode.dev](https://vscode.dev/editor/theme/Tyriar.theme-sapphire))_ This has some basic support for commands to suggest sub-commands where they would be more appropriate, such as suggesting an `npm test` rule rather than `npm`. * To improve transparency around auto-approved commands, we show which rule was applied in the Chat view, also enabling you to configure that rule: ![Screenshot showing the new links added under the tool call in the Chat view for adding new auto approve rules.](https://code.visualstudio.com/assets/updates/1_104/terminal-auto-approve-new-links.png) * We improved the defaults to provide safety and reduce noise. You can see the full list of rules by viewing the setting's default value by opening your `settings.json` file, then entering `chat.tools.terminal.autoApprove` and completing it via Tab. * Non-regex rules that contain a backslash or forward slash character are now treated as a path and not only approve that exact path, but also allow either slash type and also a `./` prefix. When using PowerShell, all rules are forced to be case insensitive. * When agent mode wants to pull content from the internet using `curl`, `wget`, `Invoke-RestMethod`, or `Invoke-WebRequest`, we now show a warning, as this is a common vector for prompt injection attacks. Learn more about [terminal auto approve](https://code.visualstudio.com/docs/copilot/chat/chat-agent-mode#_autoapprove-terminal-commands) in our documentation. #### Global auto approve Global auto approve has been [an experimental setting since v1.99](https://code.visualstudio.com/updates/v1_99#_agent-mode-tool-approvals). What we have observed is that users have been enabling this setting without thinking deeply enough about the consequences. Additionally, some users thought that enabling the `chat.tools.autoApprove` setting was a prerequisite to enabling terminal auto approve, which was never the case. To combat these misconceptions and to further protect our users, there is now a deservedly scary-looking warning the first time global auto approve attempts to be used, so the user can easily back out and disable the setting: ![Screenshot of a warning dialog that appears when global auto approve is used for the first time.](https://code.visualstudio.com/assets/updates/1_104/global-auto-approve-warning.png) The setting has also been changed to the clearer `chat.tools.global.autoApprove` without any automatic migration, so all users (accidental or intentional) need to go and explicitly set it again. #### Math rendering enabled by default **Setting**: `chat.math.enabled` Rendering of mathematical equations in chat responses is now generally available and enabled by default. You can disable this functionality with the `chat.math.enabled` setting. ![Screenshot of the Chat view, showing inline and block equations in a chat response.](https://code.visualstudio.com/assets/updates/1_104/chat-math.png) This feature is powered by [KaTeX](https://katex.org) and supports both inline and block math equations. Inline math equations can be written by wrapping the markup in single dollar signs (`$...$`), while block math equations use two dollar signs (`$$...$$`). #### Chat view default visibility **Setting**: `workbench.secondarySideBar.defaultVisibility` When you first open a workspace, the Secondary Side Bar with the Chat view is visible by default, inviting you to ask questions or start an agentic session right away. You can configure this behavior with the `workbench.secondarySideBar.defaultVisibility` setting or by using the dropdown of the Chat view itself: ![Screenshot showing Chat view menu with the option to set the default Secondary Side Bar visibility.](https://code.visualstudio.com/assets/updates/1_104/auxview.png) #### Improved task support * Input request detection When you run a task or terminal command in agent mode, the agent now detects when the process requests user input, and you're prompted to respond in chat. If you type in the terminal while a prompt is present, the prompt will hide automatically. When options and descriptions are provided (such as `[Y] Yes [N] No`), these are surfaced in the confirmation prompt. * Error detection for tasks with problem matchers For tasks that use problem matchers, the agent now collects and surfaces errors based on the problem matcher results, rather than relying on the language model to evaluate output. Problems are presented in a dropdown within the chat progress message, allowing you to navigate directly to the problem location. This ensures that errors are reported only when relevant to the current task execution. * Compound task support Agent mode now supports running compound tasks. When you run a compound task, the agent indicates progress and output for each dependent task, including any prompts for user input. This enables more complex workflows and better visibility into multi-step task execution. In the example below, the VS Code - Build task is run. Output is assessed for each dependency task and a problem is surfaced to the user in the response and in the progress message dropdown. #### Improved terminal support * Moved more terminal tools to core Like [the `runInTerminal` tool last release](https://code.visualstudio.com/updates/v1_103#_improved-reliability-and-performance-of-the-run-in-terminal-and-task-tools), the `terminalSelection` and `terminalLastCommand` tools have been moved from the extension to core, which should provide general reliability improvements. * Configurable terminal tool shell integration timeout Whenever the `runInTerminal` tool tries to create a terminal, it waits a period for shell integration to activate. If your shell is especially slow to start up, say you have a very heavy PowerShell profile, this could cause it to wait the previously fixed 5-second timeout and still end up failing in the end. This timeout is now configurable via the `chat.tools.terminal.shellIntegrationTimeout` setting. * Prevent Command Prompt usage Since shell integration isn't really possible in Command Prompt, at least with the capabilities that Copilot needs, Copilot now opts to use Windows PowerShell instead, which should have shell integration by default. This should improve the reliability of the `runInTerminal` tool when your default shell is Command Prompt. If, for some reason, you want Copilot to use Command Prompt, this is currently not possible. We will likely be adding the ability to customize the terminal profile used by Copilot soon, which is tracked in [#253945](https://github.com/microsoft/vscode/issues/253945). #### Todo List tool The todo list tool helps agents break down complex multi-step tasks into smaller tasks and report progress to help you track individual items. We've made improvements to this tool, which is now enabled by default. Tool progress is displayed in the Todo control at the top of the Chat view, which automatically collapses as the todo list is worked through and shows only the current task in progress. #### Skip tool calls When the agent requests confirmation for a tool call, you can now choose to skip the tool call and let the agent continue. You can still cancel the request or enter a new request via the chat input box. #### Improvements to semantic workspace search We've upgraded the `#codebase` tool to use a new [embeddings](https://en.wikipedia.org/wiki/Embedding_(machine_learning)) model for semantic searching for code in your workspace. This new model provides better results for code searches. The new embeddings also use less storage space, requiring only 6% of our previous model's on-disk storage size for each embedding. We'll be gradually rolling out this new embeddings model over the next few weeks. Your workspace will be automatically updated to use this new embeddings model, so no action is required. VS Code Insiders is already using the new model if you want to try it out before it rolls out to you. #### Hide and disable GitHub Copilot AI features **Setting**: `chat.disableAIFeatures` We are introducing a new setting `chat.disableAIFeatures` for disabling and hiding built-in AI features provided by GitHub Copilot, including chat, code completions, and next edit suggestions. The setting has the following advantages over the previous solution we had in place: * Syncs across your devices unless you disable this explicitly * Disables the Copilot extensions in case they are installed * Configure the setting per-profile or per-workspace, making it easy to disable AI features selectively The command to "Hide AI Features" was renamed to reflect this change and will now reveal this new setting in the settings editor. > **Note**: users that were hiding AI features previously will continue to see AI features hidden. You can update the setting in addition if you want to synchronize your choice across devices. ### MCP #### Support for server instructions VS Code now reads MCP server instructions and will include them in its base prompt. #### MCP auto discovery disabled by default **Setting**: `chat.mcp.discovery.enabled` VS Code supports [automatic discovery of MCP servers](https://code.visualstudio.com/docs/copilot/customization/mcp-servers#_add-an-mcp-server) installed in other apps like Claude Code. As MCP support has matured in VS Code, auto-discovery is now disabled by default, but you can re-enable it using the `chat.mcp.discovery.enabled` setting. #### Enable MCP **Setting**: `chat.mcp.access` The `chat.mcp.enabled` setting that previously controlled whether MCP servers could run in VS Code has been migrated to a new `chat.mcp.access` setting with more descriptive options: * `all`: allow all MCP servers to run (equivalent to the previous `true` value) * `none`: disable MCP support entirely (equivalent to the previous `false` value) ### Accessibility #### Focus chat confirmation action We've added a command, **Focus Chat Confirmation**, which focuses the confirmation dialog, if present, or announces to screen reader users that confirmation is not required. ### Code Editing #### Configurable inline suggestion delay **Setting**: `editor.inlineSuggest.minShowDelay` A new setting `editor.inlineSuggest.minShowDelay` enables you to configure how quickly inline suggestions can appear after you type. This can be useful if you find that suggestions are appearing too quickly and getting in the way of your typing. ### Notebooks #### Improved NES suggestions (Experimental) **Setting**: `github.copilot.chat.notebook.enhancedNextEditSuggestions.enabled` We are experimenting with improving the quality of next edit suggestions for notebooks. Currently, the language model has access to the contents of the active cell when generating suggestions. With the `github.copilot.chat.notebook.enhancedNextEditSuggestions.enabled` setting enabled, the language model has access to the entire notebook, enabling it to generate more accurate and higher-quality next edit suggestions. --- ## 0.30 (2025-08-07) GitHub Copilot updates from [July 2025](https://code.visualstudio.com/updates/v1_103): ### Chat #### Chat checkpoints **Setting**: `chat.checkpoints.enabled` We've introduced checkpoints that enable you to restore different states of your chat conversations. You can easily revert edits and go back to certain points in your chat conversation. This can be particularly useful if multiple files were changed in a chat session. When you select a checkpoint, VS Code reverts workspace changes and the chat history to that point. After restoring a checkpoint, you can redo that action as well! Checkpoints will be enabled by default and can be controlled with `chat.checkpoints.enabled`. #### Tool picker improvements We've totally revamped the tool picker this iteration and adopted a new component called Quick Tree to display all the tools. ![Screenshot showing the new tool picker using a quick tree, enabling collapsing and expanding nodes.](https://code.visualstudio.com/assets/updates/1_103/tool-picker-quick-tree.png) Notable features: * Expand or collapse tool sets, MCP servers, extension contributed tools, and more * Configuration options moved to the title bar * Sticky scrolling * Icon rendering Let us know what you think! #### Tool grouping (Experimental) **Setting**: `github.copilot.chat.virtualTools.threshold` The maximum number of tools that you can use for a single chat request is currently 128. Previously, you could quickly reach this limit by installing MCP servers with many tools, requiring you to deselect some tools in order to proceed. In this release of VS Code, we have enabled an experimental tool-calling mode for when the number of tools exceeds the maximum limit. Tools are automatically grouped and the model is given the ability to activate and call groups of tools. This behavior, including the threshold, is configurable via the setting `github.copilot.chat.virtualTools.threshold`. #### Terminal auto-approve improvements **Setting**: `chat.tools.terminal.autoApprove` Early terminal auto-approve settings were introduced last month. This release, the feature got many improvements. Learn more about [terminal auto-approval](https://code.visualstudio.com/docs/copilot/chat/chat-agent-mode#_autoapprove-terminal-commands-experimental) in our documentation. - We merged the `allowList` and `denyList` settings into the `chat.tools.terminal.autoApprove` setting. If you were using the old settings, you should see a warning asking you to migrate to the new setting. - Regular expression matchers now support flags. This allows case insensitivity, for example in PowerShell, where case often doesn't matter: ```jsonc "chat.tools.terminal.autoApprove": { // Deny any `Remove-Item` command, regardless of case "/^Remove-Item\\b/i": false } ``` - There was some confusion around how the sub-command matching works, this is now explained in detail in the setting's description, but we also support matching against the complete command line. ```jsonc "chat.tools.terminal.autoApprove": { // Deny any _command line_ containing a reference to what is likely a PowerShell script "/\\.ps1\\b/i": { approve: false, matchCommandLine: true } } ``` - The auto approve reasoning is now logged to the Terminal Output channel. We plan to [surface this in the UI soon](https://github.com/microsoft/vscode/issues/256780). #### Improved model management experience This iteration, we've revamped the chat provider API, which is responsible for language model access. Users are now able to select which models appear in their model picker, creating a more personalized and focused experience. ![Screenshot of the model picker showing various models from providers such as Copilot and OpenRouter](https://code.visualstudio.com/assets/updates/1_103/modelpicker.png) We plan to finalize this new API in the coming months and would appreciate any feedback. Finalization of this API will open up the extension ecosystem to implement their own model providers and further expand the bring your own key offering. #### Azure DevOps repos remote index support The [`#codebase` tool](https://code.visualstudio.com/docs/copilot/chat/copilot-chat-context#_perform-a-codebase-search) now supports remote indexes for workspaces that are linked to Azure DevOps repos. This enables `#codebase` to search for relevant snippets almost instantly without any initialization. This even works for larger repos with tens of thousands of indexable files. Previously, this feature only worked with GitHub linked repos. Remote indexes are used automatically when working in a workspace that is linked to Azure DevOps through git. Make sure you are also logged into VS Code with the Microsoft account you use to access the Azure DevOps repos. We're gradually rolling out support for this feature on the services side, so not every organization might be able to use it initially. Based on the success of the rollout, we hope to turn on remote indexing for Azure DevOps for as many organizations as possible. #### Improved reliability and performance of the run in terminal and task tools We have migrated the tools for running tasks and commands within the terminal from the Copilot extension into the core [microsoft/vscode repository](https://github.com/microsoft/vscode). This gives the tools access to lower-level and richer APIs, allowing us to fix many of the terminal hanging issues. This update also comes with the benefit of more easily implementing features going forward, as we're no longer restricted to the capabilities of the extension API, especially any changes that need custom UI within the Chat view. #### Warning about no shell integration when using chat While we strive to allow agent mode to run commands in terminals without shell integration, the experience will always be inferior as the terminal is essentially a black box at that point. Examples of issues that can occur without shell integration are: no exit code reporting and the inability to differentiate between a command idling and a prompt idling, resulting in output possibly not being reported to the agent. When the `run in terminal` tool is used but [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) is not detected, a message is displayed calling this out and pointing at the documentation. ![Screenshot of a message in the Chat view saying "Enable shell integration to improve command detection".](https://code.visualstudio.com/assets/updates/1_103/terminal-chat-si-none.png) #### Output polling for tasks and terminals The agent now waits for tasks and background terminals to complete before proceeding by using output polling. If a process takes longer than 20 seconds, you are prompted to continue waiting or move on. The agent will monitor the process for up to two minutes, summarizing the current state or reporting if the process is still running. This improves reliability when running long or error-prone commands in chat. #### Task awareness improvement Previously, the agent could only monitor active tasks. Now, it can track and analyze the output of both active and completed tasks, including those that have failed or finished running. This enhancement enables better troubleshooting and more comprehensive insights into task execution history. #### Agent awareness of user created terminals The agent now maintains awareness of all user-created terminals in the workspace. This enables it to track recent commands and access terminal output, providing better context for assisting with terminals and troubleshooting. #### Terminal inline chat improvements Terminal inline chat now better detects your active shell, even when working within subshells (for example, launching Python or Node from PowerShell or zsh). This dynamic shell detection improves the accuracy of inline chat responses by providing more relevant command suggestions for your current shell type. ![Screenshot of terminal inline chat showing node specific suggestions.](https://code.visualstudio.com/assets/updates/1_103/hello_node.png) #### Improved test runner tool The test runner tool has been reworked. It now shows progress inline within chat, and numerous bugs in the tool have been fixed. #### Edit previous requests **Setting**: `chat.editRequests` Last iteration, we enabled users to edit previous requests and rolled out a few different access points. This iteration, we've made inline edits the default behavior. Click on the request bubble to begin editing that request. You can modify attachments, change the mode and model, and resend your request with modified text. You can control the chat editing behavior with the `chat.editRequests` setting if you prefer editing via the toolbar hovers above each request. #### Open chat as maximized **Setting**: `workbench.secondarySideBar.defaultVisibility` We added two extra options for configuring the default visibility of the Secondary Side Bar to open it as maximized: * `maximizedInWorkspace`: open the Chat view as maximized when opening a new workspace * `maximized`: open the Chat view always as maximized, including in empty windows ![Screenshot that shows the Chat view maximized.](https://code.visualstudio.com/assets/updates/1_103/max-chat.png) #### Pending chat confirmation To help prevent accidentally closing a workspace where an agent session is actively changing files or responding to your request, we now show a dialog when you try to quit VS Code or close its window when a chat response is in progress: ![Screenshot of confirmation to exit with running chat.](https://code.visualstudio.com/assets/updates/1_103/confirm-chat-exit.png) #### OS notification on user action **Setting**: `chat.notifyWindowOnConfirmation` We now leverage the OS native notification system to show a toast when user confirmation is needed within a chat session. Enable this behavior with the `chat.notifyWindowOnConfirmation`. ![Screenshot of toast for confirmation of a chat agent.](https://code.visualstudio.com/assets/updates/1_103/chat-toast.png) We plan to improve this experience in the future to allow for displaying more information and for allowing you to approve directly from the toast. For now, selecting the toast focuses the window where the confirmation originated from. #### Math support in chat (Preview) **Setting**: `chat.math.enabled` Chats now have initial support for rendering mathematical equations in responses: ![Screenshot of the Chat view, showing inline and block equations in a chat response.](https://code.visualstudio.com/assets/updates/1_103/chat-math.png) This feature is powered by [KaTeX](https://katex.org) and supports both inline and block math equations. Inline math equations can be written by wrapping the markup in single dollar signs (`$...$`), while block math equations use two dollar signs (`$$...$$`). Math rendering can be enabled using `chat.math.enabled`. Currently, it is off by default but we plan to enable it in a future release, after further testing. #### Context7 integration for project scaffolding (Experimental) **Setting**: `github.copilot.chat.newWorkspace.useContext7` When you scaffold a new project with `#new` in chat, you can now make sure that it uses the latest documentation and APIs from **Context7**, if you have already installed the Context7 MCP server. ### MCP #### Server autostart and trust **Setting**: `chat.mcp.autostart:newAndOutdated` Previously, when you added or updated an MCP server configuration, VS Code would show a blue "refresh" icon in the Chat view, enabling you to manually refresh the list of tools. In the milestone, you can now configure the auto-start behavior for MCP servers, so you no longer have to manually restart the MCP server. Use the `chat.mcp.autostart:newAndOutdated` setting to control this behavior. You can also change this setting within the icon's tooltip and see which servers will be started: ![Screenshot showing the hover of the refresh MCP server icon, enabling you to configure the auto-start behavior.](https://code.visualstudio.com/assets/updates/1_103/mcp-refresh-tip.png) The first time an MCP server is started after being updated or changed, we now show a dialog asking you to trust the server. Giving trust to these servers is particularly important with autostart turned on to prevent running undesirable commands unknowingly. Learn more about [using MCP servers in VS Code](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) in our documentation. #### Client credentials flow for remote MCP servers The ideal flow for a remote MCP server that wants to support authentication is to use an auth provider that supports Dynamic Client Registration (DCR). This enables the client (VS Code) to register itself with that auth provider, so the auth flow is seamless. However, not every auth provider supports DCR, so we introduced a client-credentials flow that enables you to supply your own client ID and (optionally) client secret that will be used when taking you through the auth provider's auth flow. Here's what that looks like: * Step 1: VS Code detects that DCR can't be used, and asks if you want to do the client credentials flow: ![Screenshot of a modal dialog saying that DCR is not supported but you can provide client credentials manually.](https://code.visualstudio.com/assets/updates/1_103/mcp-auth-no-dcr1.png) > **IMPORTANT**: At this point, you would go to the auth provider's website and manually create an application registration. There you will put in the redirect URIs mentioned in the modal dialog. * Step 2: From the auth provider's portal, you will get a client ID and maybe a client secret. You'll put the client ID in the input box that appears and hit Enter: ![Screenshot of an input box to provide the client ID for the MCP server.](https://code.visualstudio.com/assets/updates/1_103/mcp-auth-no-dcr2.png) * Step 3: Then you'll put in the client secret if you have one, and hit Enter (leave blank if you don't have one) ![Screenshot of an input box to provide the optional client secret for the MCP server.](https://code.visualstudio.com/assets/updates/1_103/mcp-auth-no-dcr3.png) At that point, you'll be taken through the typical auth flow to authenticate the MCP server you're working with. #### Remove dynamic auth provider from Account menu Since the addition of remote MCP authentication, there has been a command available in the Command Palette called **Authentication: Remove Dynamic Authentication Providers**, which enables you to remove client credentials (client ID and, if available, a client secret) and all account information associated with that provider. We've now exposed this command in the Account menu. You can find it inside of an MCP server account: ![Screenshot of the Account menu showing the manage dynamic auth option in an account's submenu.](https://code.visualstudio.com/assets/updates/1_103/mcp-remove-dynamic-auth1.png) or at the root of the menu if you don't have any MCP server accounts yet: ![Screenshot of the Account menu showing the manage dynamic auth option in the root of account menu.](https://code.visualstudio.com/assets/updates/1_103/mcp-remove-dynamic-auth2.png) #### Support for `resource_link` and structured output VS Code now fully supports the latest MCP specification, version `2025-06-18`, with support for `resource_link`s and structured output in tool results. ### Editor Experience #### AI statistics (Preview) **Setting**: `editor.aiStats.enabled:true` We added an experimental feature for displaying basic AI statistics. Use the `editor.aiStats.enabled:true` to enable this feature, which is disabled by default. This feature shows you, per project, the percentage of characters that was inserted by AI versus inserted by typing. It also keeps track of how many inline and next edit suggestions you accepted during the current day. ![Screenshot showing the AI statistic hover information in the Status Bar.](https://code.visualstudio.com/assets/updates/1_103/ai-stats.png) ### Notebooks #### Notebook inline chat with agent tools **Setting**: `inlineChat.notebookAgent:true` The notebook inline chat control can now use the full suite of notebook agent tools to enable additional capabilities like running cells and installing packages into the kernel. To enable agent tools in notebooks, enable the new experimental setting `inlineChat.notebookAgent:true`. This also currently requires enabling the setting for inline chat v2 `inlineChat.enableV2:true`. --- ## 0.29 (2025-07-09) GitHub Copilot updates from [June 2025](https://code.visualstudio.com/updates/v1_102): ### Chat #### Copilot Chat is open source We're excited to announce that we've open sourced the GitHub Copilot Chat extension! The source code is now available at [`microsoft/vscode-copilot-chat`](https://github.com/microsoft/vscode-copilot-chat) under the MIT license. This marks a significant milestone in our commitment to transparency and community collaboration. By open sourcing the extension, we're enabling the community to: * **Contribute directly** to the development of AI-powered chat experiences in VS Code * **Understand the implementation** of chat modes, custom instructions, and AI integrations * **Build upon our work** to create even better AI developer tools * **Participate in shaping the future** of AI-assisted coding You can explore the repository to see how features like [agent mode](https://github.com/microsoft/vscode-copilot-chat/blob/e1222084830244174e6aa64683286561fa7e7607/src/extension/prompts/node/agent/agentPrompt.tsx), [inline chat](https://github.com/microsoft/vscode-copilot-chat/blob/e1222084830244174e6aa64683286561fa7e7607/src/extension/prompts/node/inline/inlineChatEditCodePrompt.tsx), and [MCP integration](https://github.com/microsoft/vscode-copilot-chat/blob/e1222084830244174e6aa64683286561fa7e7607/src/extension/mcp/vscode-node/mcpToolCallingLoop.tsx) are implemented. We welcome contributions, feedback, and collaboration from the community. To learn more about this milestone and our broader vision for open source AI editor tooling, read our detailed blog post: [Open Source AI Editor - First Milestone](https://code.visualstudio.com/blogs/2025/06/30/openSourceAIEditorFirstMilestone). #### Chat mode improvements Last milestone, we previewed [custom chat modes](https://code.visualstudio.com/docs/copilot/chat/chat-modes#_custom-chat-modes). In addition to the built-in chat modes 'Ask', 'Edit' and 'Agent', you can define your own chat modes with specific instructions and a set of allowed tools that you want the LLM to follow when replying to a request. This milestone, we have made several improvements and bug fixes in this area. ##### Configure language model Upon popular request, you can now also specify which language model should be used for a chat mode. Add the `model` metadata property to your `chatmode.md` file and provide the model identifier (we provide IntelliSense for the model info). ![Screenshot that shows the IntelliSense for the model metadata property in chat mode file.](https://code.visualstudio.com/assets/updates/1_102/prompt-file-model-code-completion.png) ##### Improved editing support The editor for [chat modes](https://code.visualstudio.com/docs/copilot/chat/chat-modes), [prompts](https://code.visualstudio.com/docs/copilot/copilot-customization#_prompt-files-experimental), and [instruction files](https://code.visualstudio.com/docs/copilot/copilot-customization#_custom-instructions) now supports completions, validation, and hovers for all supported metadata properties. ![Screenshot that shows the hover information for tools.](https://code.visualstudio.com/assets/updates/1_102/tools-hover.png) ![Screenshot that shows the model diagnostics when a model is not available for a specific chat mode.](https://code.visualstudio.com/assets/updates/1_102/prompt-file-diagnostics.png) ##### Gear menu in the chat view The **Configure Chat** action in the Chat view toolbar lets you manage custom modes as well as reusable instructions, prompts, and tool sets: ![Screenshot that shows the Configure Chat menu in the Chat view.](https://code.visualstudio.com/assets/updates/1_102/chat-gear.png) Selecting **Modes** shows all currently installed custom modes and enables you to open, create new, or delete modes. ##### Import modes via a `vscode` link You can now import a chat mode file from an external link, such as a gist. For example, the following link will import the chat mode file for Burke's GPT 4.1 Beast Mode: [Burke's GPT 4.1 Beast Mode (VS Code)](vscode:chat-mode/install?url=https://gist.githubusercontent.com/burkeholland/a232b706994aa2f4b2ddd3d97b11f9a7/raw/6e497f4b4ef5e7ea36787ef38fdf4385433591c1/4.1.chatmode.md) This will prompt for a destination folder and a name for the mode and then import the mode file from the URL in the link. The same mechanism is also available for prompt and instruction files. #### Generate custom instructions Setting up [custom instructions](https://code.visualstudio.com/docs/copilot/copilot-customization) for your project can significantly improve AI suggestions by providing context about your coding standards and project conventions. However, creating effective instructions from scratch might be challenging. This milestone, we're introducing the **Chat: Generate Instructions** command to help you bootstrap custom instructions for your workspace. Run this command from the Command Palette or the Configure menu in the Chat view, and agent mode will analyze your codebase to generate tailored instructions that reflect your project's structure, technologies, and patterns. The command creates a `copilot-instructions.md` file in your `.github` folder or suggests improvements to existing instruction files. You can then review and customize the generated instructions to match your team's specific needs. Learn more about [customizing AI responses with instructions](https://code.visualstudio.com/docs/copilot/copilot-customization). #### Load instruction files on demand Instruction files can be used to describe coding practices and project requirements. Instructions can be manually or automatically included as context to chat requests. There are various mechanisms supported, see the [Custom Instructions](https://code.visualstudio.com/docs/copilot/copilot-customization#_custom-instructions) section in our documentation. For larger instructions that you want to include conditionally, you can use `.instructions.md` files in combination with glob patterns defined in the `applyTo` header. The file is automatically added when the glob pattern matches one or more of the files in the context of the chat. New in this release, the large language model can load instructions on demand. Each request gets a list of all instruction files, along with glob pattern and description. In this example, the LLM has no instructions for TypeScript files explicitly added in the context. So, it looks for code style rules before creating a TypeScript file: ![Screenshot showing loading instruction files on demand.](https://code.visualstudio.com/assets/updates/1_102/instructions-loading-on-demand.png) #### Edit previous requests (Experimental) You can now click on previous requests to modify the text content, attached context, mode, and model. Upon submitting this change, this will remove all subsequent requests, undo any edits made, and send the new request in chat. There will be a controlled rollout of different entry points to editing requests, which will help us gather feedback on preferential edit and undo flows. However, users can set their preferred mode with the experimental `chat.editRequests` setting: * `chat.editRequests.inline`: Hover a request and select the text to begin an edit inline with the request. * `chat.editRequests.hover`: Hover a request to reveal a toolbar with a button to begin an edit inline with the request. * `chat.editRequests.input`: Hover a request to reveal a toolbar, which will start edits in the input box at the bottom of chat. #### Terminal auto-approval (Experimental) Agent mode now has a mechanism for auto approving commands in the terminal. Here's a demo of it using the defaults: There are currently two settings: the allow list and the deny list. The allow list is a list of command _prefixes_ or regular expressions that when matched allows the command to be run without explicit approval. For example, the following will allow any command starting with `npm run test` to be run, as well as _exactly_ `git status` or `git log`: ```json "github.copilot.chat.agent.terminal.allow list": { "npm run test": true, "/^git (status|log)$/": true } ``` These settings are merged across setting scopes, such that you can have a set of user-approved commands, as well as workspace-specific approved commands. As for chained commands, we try to detect these cases based on the shell and require all sub-commands to be approved. So `foo && bar` we check that both `foo` and `bar` are allowed, only at that point will it run without approval. We also try to detect inline commands such as `echo $(pwd)`, which would check both `echo $(pwd)` and `pwd`. The deny list has the same format as the allow list but will override it and force approval. For now this is mostly of use if you have a broad entry in the allow list and want to block certain commands that it may include. For example the following will allow all commands starting with `npm run` except if it starts with `npm run danger`: ```json "github.copilot.chat.agent.terminal.allow list": { "npm run": true }, "github.copilot.chat.agent.terminal.denyList": { "npm run danger": true } ``` Thanks to the protections that we gain against prompt injection from [workspace trust](https://code.visualstudio.com/docs/editing/workspaces/workspace-trust), the philosophy we've approached when implementing this feature with regards to security is to include a small set of innocuous commands in the allow list, and a set of particularly dangerous ones in the deny list just in case they manage to slip through. We're still considering what should be the defaults but here is the current lists: * Allow list: `echo`, `cd`, `ls`, `cat`, `pwd`, `Write-Host`, `Set-Location`, `Get-ChildItem`, `Get-Content`, `Get-Location` * Deny list: `rm`, `rmdir`, `del`, `kill`, `curl`, `wget`, `eval`, `chmod`, `chown`, `Remove-Item` The two major parts we want to add to this feature are a UI entry point to more easily add new commands to the list ([#253268](https://github.com/microsoft/vscode/issues/253268)) and an opt-in option to allow an LLM to evaluate the command(s) safety ([#253267](https://github.com/microsoft/vscode/issues/253267)). We are also planning on both removing the `github.copilot.` prefix of these settings ([#253314](https://github.com/microsoft/vscode/issues/253314)) as well as merging them together ([#253472](https://github.com/microsoft/vscode/issues/253472)) in the next release before it becomes a preview setting. #### Terminal command simplification Agent mode sometimes wants to run commands with a `cd` statement, just in case. We now detect this case when it matches the current working directory and simplify the command that is run. ![Screenshot of the terminal, asking to run `cd C:\Github\Tyriar\xterm.js && echo hello` only runs `echo hello` when the current working directory already matches.](https://code.visualstudio.com/assets/updates/1_102/terminal-working-dir.png) #### Agent awareness of tasks and terminals Agent mode now understands which background terminals it has created and which tasks are actively running. The agent can read task output by using the new `GetTaskOutput` tool, which helps prevent running duplicate tasks and improves workspace context. ![Screenshot of VS Code window showing two build tasks running in the terminal panel. The left terminal displays several errors. The chat agent replies to describe status of my build tasks with a summary of each task's output.](https://code.visualstudio.com/assets/updates/1_102/task-status.png) #### Maximized chat view You can now maximize the Secondary Side Bar to span the editor area and hide the Primary Side Bar and panel area. VS Code will remember this state between restarts and will restore the Chat view when you open an editor or view. You can toggle in and out of the maximized state by using the new icon next to the close button, or use the new command `workbench.action.toggleMaximizedAuxiliaryBar` from the Command Palette. #### Agent mode badge indicator We now show a badge over the application icon in the dock when the window is not focused and the agent needs user confirmation to continue. The badge will disappear as soon as the related window that triggered it receives focus. ![Screenshot of the VS Code dock icon showing an agent confirmation as a badge.](https://code.visualstudio.com/assets/updates/1_102/badge.png) You can enable or disable this badge via the `chat.notifyWindowOnConfirmation` setting. #### Start chat from the command line A new subcommand `chat` is added to the VS Code CLI that enables you to start a chat session in the current working directory with the prompt provided. The basic syntax is `code chat [options] [prompt]` and options can be any of: * `-m --mode `: The mode to use for the chat session. Available options: 'ask', 'edit', 'agent', or the identifier of a custom mode. Defaults to 'agent' * `-a --add-file `: Add files as context to the chat session * `--maximize`: Maximize the chat session view * `-r --reuse-window`: Force to use the last active window for the chat session * `-n --new-window`: Force to open an empty window for the chat session Reading from stdin is supported, provided you pass in `-` at the end, for example `ps aux | grep code | code chat -` #### Fetch tool supports non-HTTP URLs We've seen that, on occasion, models want to call the Fetch tool with non-HTTP URLs, such as `file://` URLs. Rather than disallowing this, the Fetch tool now supports these URLs and returns the content of the file or resource at the URL. Images are also supported. #### Clearer language model access management We've reworked the UX around managing extension access to language models provided by extensions. Previously, you saw an item in the Account menu that said **AccountName (GitHub Copilot Chat)**, which had nothing to do with what account GitHub Copilot Chat was using. Rather, it allowed you to manage which extensions had access to the language models provided by Copilot Chat. To make this clearer, we've removed the **AccountName (GitHub Copilot Chat)** item and replaced it with a new item called **Manage Language Model Access...**. This item opens a Quick Pick that enables you to manage which extensions have access to the language models provided by GitHub Copilot Chat. ![Screenshot that shows the language model access Quick Pick.](https://code.visualstudio.com/assets/updates/1_102/lm-access-qp.png) We think this is clearer... That said, in a future release we will explore more granular access control for language models (for example, only allowing specific models rather than _all_ models provided by an extension), so stay tuned for that. ### MCP #### MCP support in VS Code is generally available We've have been working on expanding MCP support in VS Code for the past few months, and [support the full range of MCP features in the specification](https://code.visualstudio.com/blogs/2025/06/12/full-mcp-spec-support). As of this release, MCP support is now generally available in VS Code! You can get started by installing some of the [popular MCP servers from our curated list](https://code.visualstudio.com/mcp). Learn more about [using MCP servers in VS Code](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) and how you can use them to extend agent mode. ![Screenshot that shows the MCP Servers page.](https://code.visualstudio.com/assets/updates/1_102/mcp-servers-page.png) If you want to build your own MCP server, check our [MCP developer guide](https://code.visualstudio.com/api/extension-guides/ai/mcp) for more details about how to take advantage of the MCP capabilities in VS Code. #### Support for elicitations The latest MCP specification added support for [Elicitations](https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation) as a way for MCP servers to request input from MCP clients. The latest version of VS Code adopts this specification and includes support for elicitations. #### MCP server discovery and installation The new **MCP Servers** section in the Extensions view includes welcome content that links directly to the [popular MCP servers from our curated list](https://code.visualstudio.com/mcp). Visit the website to explore available MCP servers and select **Install** on any MCP server. This automatically launches VS Code and opens the MCP server editor that displays the server's readme and manifest information. You can review the server details and select **Install** to add the server to your VS Code instance. Once installed, MCP servers automatically appear in your Extensions view under the **MCP SERVERS - INSTALLED** section, and their tools become available in the Chat view's tools Quick Pick. This makes it easy to verify that your MCP server is working correctly and access its capabilities immediately. #### MCP server management view The new **MCP SERVERS - INSTALLED** view in the Extensions view makes it easy to monitor, configure, and control your installed MCP servers. ![Screenshot showing the MCP Servers management view with installed servers.](https://code.visualstudio.com/assets/updates/1_102/mcp-servers-installed-view.png) The view lists the installed MCP servers and provides several management actions through the context menu: ![Screenshot showing the context menu actions for an MCP server.](https://code.visualstudio.com/assets/updates/1_102/mcp-server-context-menu.png) * **Start Server** / **Stop Server** / **Restart Server**: Control the server's running state * **Disconnect Account**: Remove account access from the server * **Show Output**: View the server's output logs for troubleshooting * **Show Configuration**: Open the server's runtime configuration * **Configure Model Access**: Manage which language models the server can access * **Show Sampling Requests**: View sampling requests for debugging * **Browse Resources**: Explore resources provided by the server * **Uninstall**: Remove the server from your VS Code instance When you select an installed MCP server, VS Code opens the MCP server editor displaying the server's readme details, manifest, and its runtime configuration. This provides an overview of the server's capabilities and current settings, making it easy to understand what the server does and how it's configured. ![Screenshot showing the MCP server editor with runtime configuration.](https://code.visualstudio.com/assets/updates/1_102/mcp-server-editor-configuration.png) The **MCP SERVERS - INSTALLED** view also provides a **Browse MCP Servers...** action that takes you directly to the community website, making server discovery always accessible from within VS Code. ![Screenshot that shows the Browse MCP Servers action in the MCP Servers view.](https://code.visualstudio.com/assets/updates/1_102/mcp-servers-browse-action.png) #### MCP servers as first-class resources MCP servers are now treated as first-class resources in VS Code, similar to user tasks and other profile-specific configurations. This represents a significant architectural improvement from the previous approach where MCP servers were stored in user settings. This change makes MCP server management more robust and provides better separation of concerns between your general VS Code settings and your MCP server configurations. When you install or configure MCP servers, they're automatically stored in the appropriate [profile](https://code.visualstudio.com/docs/configure/profiles)-specific location to ensure that your main settings file stays clean and focused. * **Dedicated storage**: MCP servers are now stored in a dedicated `mcp.json` file within each profile, rather than cluttering your user settings file * **Profile-specific**: Each VS Code profile maintains its own set of MCP servers, enabling you to have different server configurations for different workflows or projects * **Settings Sync integration**: MCP servers sync seamlessly across your devices through [Settings Sync](https://code.visualstudio.com/docs/configure/settings-sync), with granular control over what gets synchronized ##### MCP migration support With MCP servers being first-class resources and the associated change to their configuration, VS Code provides comprehensive migration support for users upgrading from the previous MCP server configuration format: * **Automatic detection**: Existing MCP servers in `settings.json` are automatically detected and migrated to the new profile-specific `mcp.json` format * **Real-time migration**: When you add MCP servers to user settings, VS Code immediately migrates them with a helpful notification explaining the change * **Cross-platform support**: Migration works seamlessly across all development scenarios including local, remote, WSL, and Codespaces environments This migration ensures that your existing MCP server configurations continue to work without any manual intervention while providing the enhanced management capabilities of the new architecture. ##### Dev Container support for MCP configuration The Dev Container configuration `devcontainer.json` and the Dev Container Feature configuration `devcontainer-feature.json` support MCP server configurations at the path `customizations.vscode.mcp`. When a Dev Container is created the collected MCP server configurations are written to the remote MCP configuration file `mcp.json`. ##### Commands to access MCP resources To make working with MCP servers more accessible, we've added commands to help you manage and access your MCP configuration files: * **MCP: Open User Configuration** - Direct access to your user-level `mcp.json` file * **MCP: Open Remote User Configuration** - Direct access to your remote user-level `mcp.json` file These commands provide quick access to your MCP configuration files, making it easy to view and manage your server configurations directly. #### Quick management of MCP authentication You are now able to sign out or disconnect accounts from the MCP gear menu and quick picks. * MCP view gear menu: ![Screenshot showing the Disconnect Account action shown in MCP view gear menu.](https://code.visualstudio.com/assets/updates/1_102/mcp-view-signout.png) * MCP editor gear menu: ![Screenshot showing the Disconnect Account action shown in MCP editor gear menu.](https://code.visualstudio.com/assets/updates/1_102/mcp-editor-signout.png) * MCP quick pick: ![Screenshot showing the Disconnect Account action shown in MCP quick pick menu.](https://code.visualstudio.com/assets/updates/1_102/mcp-qp-signout.png) The **Disconnect** action is shown when the account is used by either other MCP servers or extensions, while **Sign Out** is shown when the account is only used by the MCP server. The sign out action completely removes the account from VS Code, while disconnect only removes access to the account from the MCP server. ### Code Editing #### Snooze code completions You can now temporarily pause inline suggestions and next edit suggestions (NES) by using the new **Snooze** feature. This is helpful when you want to focus without distraction from suggestions. To snooze suggestions, select the Copilot dashboard in the Status Bar, or run the **Snooze Inline Suggestions** command from the Command Palette and select a duration from the dropdown menu. During the snooze period, no inline suggestions or NES will appear. ![Screenshot showing the Copilot dashboard with the snooze button at the bottom.](https://code.visualstudio.com/assets/updates/1_102/nes-snooze.png) You can also assign a custom keybinding to quickly snooze suggestions for a specific duration by passing the desired duration as an argument to the command. For example: ```json { "key": "...", "command": "editor.action.inlineSuggest.snooze", "args": 10 } ``` ### Editor Experience #### Settings search suggestions (Preview) **Setting**: `workbench.settings.showAISearchToggle` This milestone, we modified the sparkle toggle in the Settings editor, so that it acts as a toggle between the AI and non-AI search results. The AI settings search results are semantically similar results instead of results that are based on string matching. For example, `editor.fontSize` appears as an AI settings search result when you search for "increase text size". The toggle is enabled only when there are AI results available. We welcome feedback on when the AI settings search did not find an expected setting, and we plan to enable the setting by default over the next iteration. --- ## 0.28 (2025-06-12) GitHub Copilot updates from [May 2025](https://code.visualstudio.com/updates/v1_101): ### Chat #### Chat tool sets VS Code now enables you to define tool sets, either through a proposed API or through the UI. A tool set is a collection of different tools that can be used just like individual tools. Tool sets make it easier to group related tools together, and quickly enable or disable them in agent mode. For instance, the tool set below is for managing GitHub notifications (using the [GitHub MCP server](https://github.com/github/github-mcp-server)). ```json { "gh-news": { "tools": [ "list_notifications", "dismiss_notification", "get_notification_details", ], "description": "Manage GH notification", "icon": "github-project" } } ``` To create a tool set, run the **Configure Tool Sets** > **Create new tool sets file** command from the Command Palette. You can then select the tools you want to include in the tool set, and provide a description and icon. To use a tool set in a chat query, reference it by #-mentioning its name, like `#gh-news`. You can also choose it from the tool picker in the chat input box. ![Screenshot of the Chat view showing a query about unread notifications, using the 'gh-news' tool set highlighted in both the chat interface and a JSON configuration file which defines this tool set.](https://code.visualstudio.com/assets/updates/1_101/tool-set-gh.png) Learn more about [tools sets](https://code.visualstudio.com/docs/copilot/chat/chat-agent-mode#_define-tool-sets) in our documentation. #### MCP support for prompts VS Code's Model Context Protocol support now includes prompt support. Prompts can be defined by MCP servers to generate reusable snippets or tasks for the language model. Prompts are accessible as slash `/` commands in chat, in the format `/mcp.servername.promptname`. You can enter plain text or include command output in prompt variables, and we also support completions when servers provide it. The following example shows how we generate a prompt using AI, save it using the [Gistpad MCP server](https://github.com/lostintangent/gistpad-mcp), and then use it to generate a changelog entry: #### MCP support for resources VS Code's Model Context Protocol support now includes resource support, which includes support for resource templates. It is available in several places: 1. Resources returned from MCP tool calls are available to the model and can be saved in chat, either via a **Save** button or by dragging the resource onto the Explorer view. 1. Resources can be attached as context via the **Add Context...** button in chat, then selecting **MCP Resources...**. 1. You can browse and view resources across servers using the **MCP: Browse Resources** command or for a server by its entry in the **MCP: List Servers** command. Here's an example of attaching resources from the [Gistpad MCP server](https://github.com/lostintangent/gistpad-mcp) to chat: #### MCP support for sampling (Experimental) VS Code's Model Context Protocol support now includes sampling, which allows MCP servers to make requests back to the model. You'll be asked to confirm the first time an MCP server makes a sampling request, and you can configure the models the MCP server has access to as well as see a request log by selecting the server in **MCP: List Servers.** Sampling support is still preliminary and we plan to expand and improve it in future iterations. #### MCP support for auth VS Code now supports MCP servers that require authentication, allowing you to interact with an MCP server that operates on behalf of your user account for that service. This feature implements the MCP authorization specification for clients, and supports both: * [2025-3-26 spec](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization), where the MCP server behaves as an authorization server. * [Draft spec](https://modelcontextprotocol.io/specification/draft/basic/authorization), where the MCP server behaves as a resource server (this is expected to be finalized any day now). If the MCP server implements the draft spec and leverages GitHub or Entra as the auth server, you can manage which MCP servers have access to your account: ![Screenshot of the "Manage Trusted MCP Servers" option in the account menu.](https://code.visualstudio.com/assets/updates/1_101/manage-trusted-mcp.png) ![Screenshot of the "Manage Trusted MCP Servers" Quick Pick.](https://code.visualstudio.com/assets/updates/1_101/manage-trusted-mcp-quick-pick.png) You can also manage which account that server should use (via the gear button in the previous quick pick): ![Screenshot of the "Account Preference" Quick Pick.](https://code.visualstudio.com/assets/updates/1_101/account-pref-quick-pick.png) For other MCP servers that rely on dynamic client registration, we include the auth state in the same place as everything else, for example with Linear: ![Screenshot of Linear appearing in the account menu.](https://code.visualstudio.com/assets/updates/1_101/linear-account-menu.png) There you can also sign out. For these we support not only the code authorization flow but also the device code flow should your authorization server support it. We have also introduced the command `Authentication: Remove Dynamic Authentication Providers` that allows you to clean up any of these dynamic client registrations. This will throw away the client id issued to VS Code and all data associated with this authentication provider. Remember, you can use the **MCP: Add Server...** command to add MCP servers. This is the same entry point for servers with authentication. #### MCP development mode You can enable _development mode_ for MCP servers by adding a `dev` key to the server config. This is an object with two properties: * `watch`: A file glob pattern to watch for files change that will restart the MCP server. * `debug`: Enables you to set up a debugger with the MCP server. Currently, we only support debugging Node.js and Python servers launched with `node` and `python` respectively. **.vscode/mcp.json** ```diff { "servers": { "gistpad": { "command": "node", "args": ["build/index.js"], + "dev": { + "watch": "build/**/*.js", + "debug": { "type": "node" } + }, ``` #### Chat UX improvements We're continuously working to improve the chat user experience in VS Code based on your feedback. One such feedback was that it can be difficult to distinguish between user messages and AI responses in the chat. To address this, we've made the appearance of user messages more distinct. Undoing previous requests is now also more visible - just hover over a request and select the `X` button to undo that request and any following requests. Or even quicker, use the Delete keyboard shortcut! Finally, attachments from the chat input box are now more navigable. Learn more about using [chat in VS Code](https://code.visualstudio.com/docs/copilot/chat/copilot-chat) in our documentation. #### Apply edits more efficiently When editing files, VS Code can take two different approaches: it either rewrites the file top to bottom or it makes multiple, smaller edits. Both approaches differ, for example the former can be slower for large files and intermediate states do often not compile successfully. Because of that the UI adopts and conditionally disables auto-save and squiggles, but only when needed. We have also aligned the keybindings for the **Keep** and **Undo** commands. Keeping and undoing individual changes is now done with Ctrl+Y and Ctrl+N. In the same spirit, we have also aligned the keybinding for keeping and undoing all changes in a file, they are now Ctrl+Shift+Y and Ctrl+Shift+N. This is not just for alignment but also removes prior conflicts with popular editing commands (like **Delete All Left**). #### Implicit context We've streamlined and simplified the way that adding your current file as context works in chat. Many people found the "eyeball toggle" that we previously had to be a bit clunky. Now, your current file is offered as a suggested context item. Just select the item to add or remove it from chat context. From prompt input field, press `Shift+Tab, Enter` to quickly do this with the keyboard. Additionally, in agent mode, we include a hint about your current editor. This doesn't include the contents of the file, just the file name and cursor position. The agent can then use the tools it has to read the contents of the file on its own, if it thinks that it's relevant to your query. Learn more about [adding context in chat](https://code.visualstudio.com/docs/copilot/chat/copilot-chat-context) in our documentation. #### Fix task configuration errors Configuring tasks and problem matchers can be tricky. Use the **Fix with Github Copilot** action that is offered when there are errors in your task configuration to address them quickly and efficiently. #### Custom chat modes (Preview) By default, the chat view supports three built-in chat modes: Ask, Edit and Agent. Each chat mode comes with a set of base instructions that describe how the LLM should handle a request, as well as the list of tools that can be used for that. You can now define your own custom chat modes, which can be used in the Chat view. Custom chat modes allow you to tailor the behavior of chat and specify which tools are available in that mode. This is particularly useful for specialized workflows or when you want to provide specific instructions to the LLM. For example, you can create a custom chat mode for planning new features, which only has read-only access to your codebase. To define and use a custom chat mode, follow these steps: 1. Define a custom mode by using the **Chat: Configure Chat Modes** command from the Command Palette. 1. Provide the instructions and available tools for your custom chat mode in the `*.chatprompt.md` file that is created. 1. In the Chat view, select the chat mode from the chat mode dropdown list. 1. Submit your chat prompt and ![Screenshot of the custom chat mode selected in the Chat view.](https://code.visualstudio.com/assets/updates/1_101/custom-chat-mode-view.png) The following example shows a custom "Planning" chat mode: ```md --- description: Generate an implementation plan for new features or refactoring existing code. tools: ['codebase', 'fetch', 'findTestFiles', 'githubRepo', 'search', 'usages'] --- # Planning mode instructions You are in planning mode. Your task is to generate an implementation plan for a new feature or for refactoring existing code. Don't make any code edits, just generate a plan. The plan consists of a Markdown document that describes the implementation plan, including the following sections: * Overview: A brief description of the feature or refactoring task. * Requirements: A list of requirements for the feature or refactoring task. * Implementation Steps: A detailed list of steps to implement the feature or refactoring task. * Testing: A list of tests that need to be implemented to verify the feature or refactoring task. ``` > **Note**: The feature is work in progress, but please try it out! Please follow the latest progress in VS Code Insiders and let us know what's not working or is missing. #### Task diagnostic awareness When the chat agent runs a task, it is now aware of any errors or warnings identified by problem matchers. This diagnostic context allows the chat agent to respond more intelligently to issues as they arise. #### Terminal cwd context When agent mode has opened a terminal and shell integration is active, the chat agent is aware of the current working directory (cwd). This enables more accurate and context-aware command support. #### Floating window improvements When you move a chat session into a floating window, there are now two new actions available in the title bar: * Dock the chat back into the VS Code window where it came from * Start a new chat session in the floating window. ![Screenshot of the Chat view in a floating window, highlighting the Dock and New Chat buttons in the title bar.](https://code.visualstudio.com/assets/updates/1_101/chat-floating.png) #### Fetch tool confirmation The fetch tool enables you to pull information from a web page. We have added a warning message to the confirmation to inform you about potential prompt injection. ![Screenshot of the fetch tool with a warning about prompt injection.](https://code.visualstudio.com/assets/updates/1_101/fetch-warning.png) #### Customize more built-in tools It's now possible to enable or disable all built-in tools in agent mode or your custom mode. For example, disable `editFiles` to disallow agent mode to edit files directly, or `runCommands` for running terminal commands. In agent mode, select the **Configure Tools** button to open the tool picker, and select your desired set of tools. ![Screenshot of the tool picker, showing the "editFiles" tool set item cleared.](https://code.visualstudio.com/assets/updates/1_101/built-in-toolsets.png) Some of the entries in this menu represent tool sets that group multiple tools. For example, we give the model multiple tools to edit or create text files and notebooks, which may also differ by model family, and `editFiles` groups all of these. #### Send elements to chat (Experimental) Last milestone, we added a [new experimental feature](https://code.visualstudio.com/updates/v1_100#_select-and-attach-ui-elements-to-chat-experimental) where you could open the Simple Browser and select web elements to add to chat from the embedded browser. ![Screenshot showing the Live Preview extension, highlighting the overlay controls to select web elements from the web page.](https://code.visualstudio.com/assets/updates/1_101/live-preview-select-web-elements.png) As we continue to improve this feature, we have added support for selecting web elements in the [Live Preview extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.live-server) as well. Check this out by downloading the extension and spinning up a live server from any HTML file. ### Accessibility #### User action required sound We’ve added an accessibility signal to indicate when chat requires user action. This is opt-in as we fine tune the sound. You can configure this behavior with `accessibility.signals.chatUserActionRequired`. #### New code action sounds We’ve introduced distinct sounds for when a code action is triggered (`accessibility.signals.codeActionTriggered)`) and when it is applied (`accessibility.signals.codeActionApplied`. #### Agent mode accessibility improvements We now include rich information about confirmation dialogs in the accessible view, covering past tool runs, the current tool run, and any pending confirmations. This includes the inputs that will be used. When a confirmation dialog appears in a response, the action’s title is now included in the ARIA label of the corresponding code block, the response’s ARIA label, and the live alert to provide better context for screen reader users. ### Editor Experience #### Settings search suggestions (Preview) **Setting**: `workbench.settings.showAISearchToggle:true` This milestone, we added a toggle to the Settings editor that starts an AI search to find semantically similar results instead of results that are based on string matching. For example, the AI search finds the `editor.fontSize` setting when you search for "increase text size". To see the toggle, enable the setting and reload VS Code. We are also in the process of identifying and fixing some of the less accurate settings search results, and we welcome feedback on when a natural language query did not find an expected setting. For the next milestone, we are also considering removing the toggle and changing the experimental setting to one that controls when to directly append the slower AI search results to the end of the list. #### Search keyword suggestions (Preview) **Setting**: `search.searchView.keywordSuggestions` Last milestone, we introduced [keyword suggestions](https://code.visualstudio.com/updates/v1_100#_semantic-text-search-with-keyword-suggestions-experimental) in the Search view to help you find relevant results faster. We have now significantly improved the performance of the suggestions, so you will see the results ~5x faster than before. We have also moved the setting from the Chat extension into VS Code core, and renamed it from `github.copilot.chat.search.keywordSuggestions` to `search.searchView.keywordSuggestions`. #### Semantic search behavior options (Preview) **Setting**: `search.searchView.semanticSearchBehavior` With semantic search in the Search view, you can get results based on the meaning of your query rather than just matching text. This is particularly useful if you don't know the exact terms to search for. By default, semantic search is only run when you explicitly request it. We have now added a setting to control when you want semantic search to be triggered: * `manual` (default): only run semantic search when triggered manually via the button or keyboard shortcut Ctrl+I * `runOnEmpty`: run semantic search automatically when the text search returns no results * `auto`: automatically run semantic search in parallel with text search for every search query ### Code Editing #### NES import suggestions **Setting**: `github.copilot.nextEditSuggestions.fixes` Last month, we introduced support for next edit suggestions to automatically suggest adding missing import statements for TypeScript and JavaScript. In this release, we've improved the accuracy and reliability of these suggestions and expanded support to Python files as well. Additionally, NES is now enabled by default for all users. ![Screenshot showing NES suggesting an import statement.](https://code.visualstudio.com/assets/updates/1_100/nes-import.png) #### NES acceptance flow Accepting next edit suggestions is now more seamless. Once you accept a suggestion, you can continue accepting subsequent suggestions with a single Tab press, as long as you haven't started typing again. If you start typing, you'll need to press Tab to first move the cursor to the next suggestion before you can accept it. ### Notebooks #### Follow mode for agent cell execution **Setting**: `github.copilot.chat.notebook.followCellExecution.enabled` With follow mode, the Notebook view will automatically scroll to the cell that is currently being executed by the agent. Use the `github.copilot.chat.notebook.followCellExecution.enabled` setting to enable or disable follow mode for agent cell execution in Jupyter Notebooks. Once the agent has used the run cell tool, the Notebook toolbar is updated with a pin icon, indicating the state of follow mode. You can toggle the behavior mid agent response without changing the base setting value, allowing you to follow the work of the agent in real-time, and toggle it off when you want to review a specific portion of code while the agent continues to iterate. When you wish to follow again, simply toggle the mode, and join at the next execution. #### Notebook tools for agent mode ##### Configure notebook The [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) contributes tools for configuring the Kernel of a Jupyter Notebook. This tool ensures that a Kernel is selected and is ready for use in the Notebook. This involves walking you through the process of creating a Virtual Environment if required (the recommended approach), or prompting you to select an existing Python environment. This tool ensures the LLM can perform operations on the Notebook such as running cells with minimal user interaction, thereby improving the overall user experience in agent mode. ##### Long running agent workflows The agent has access to an internal Notebook Summary tool to help keep it on track with an accurate context. That summary is also included when summarizing the conversation history when the context gets too large to keep the agent going through complex operations. ##### Cell preview in run confirmation A snippet of the code is shown from a notebook cell when the agent requests confirmation to run that cell. The cell links in the Chat view now also enable you to directly navigate to cells in the notebook. ### Source Control #### Copilot coding agent integration With Copilot coding agent, GitHub Copilot can work independently in the background to complete tasks, just like a human developer. We have expanded the GitHub Pull Requests extension to make it easier to assign and track tasks for the agent from within VS Code. We have added the following features to the extension: * **Assign to Copilot**: assign a pull request or issue to Copilot from the issue or PR view in VS Code * **Copilot on My Behalf** PR query: quickly see all pull requests that Copilot is working on for you. * **PR view**: see the status of the Copilot coding agent and open the session details in the browser. #### Add history item to chat context You can now add a source control history item as context to a chat request. This can be useful when you want to provide the contents of a specific commit or pull request as context for your chat prompt. ![Screenshot of the Chat view input box that has a history item added as context.](https://code.visualstudio.com/assets/updates/1_101/chat-context-source-control-commit.png) To add a history item to chat, use **Add Context** > **Source Control** from the Chat view and then choose a particular history item. Alternatively, right-click the history item in the source control graph and then select **Copilot** > **Add History Item to Chat** from the context menu. --- ## 0.27 (2025-05-07) GitHub Copilot updates from [April 2025](https://code.visualstudio.com/updates/v1_100): ### Chat #### Prompt and instructions files You can tailor your AI experience in VS Code to your specific coding practices and technology stack by using Markdown-based instructions and prompt files. We've aligned the implementation and usage of these two related concepts, however they each have distinct purposes. ##### Instructions files **Setting**: `chat.instructionsFilesLocations` Instructions files (also known as custom instructions or rules) provide a way to describe common guidelines and context for the AI model in a Markdown file, such as code style rules, or which frameworks to use. Instructions files are not standalone chat requests, but rather provide context that you can apply to a chat request. Instructions files use the `.instructions.md` file suffix. They can be located in your user data folder or in the workspace. The `chat.instructionsFilesLocations` setting lists the folders that contain instruction files. You can manually attach instructions to a specific chat request, or they can be automatically added: * To add them manually, use the **Add Context** button in the Chat view, and then select **Instructions...**. Alternatively use the **Chat: Attach Instructions...** command from the Command Palette. This brings up a picker that lets you select existing instructions files or create a new one to attach. * To automatically add instructions to a prompt, add the `applyTo` Front Matter header to the instructions file to indicate which files the instructions apply to. If a chat request contains a file that matches the given glob pattern, the instructions file is automatically attached. The following example provides instructions for TypeScript files (`applyTo: '**/*.ts'`): ````md --- applyTo: '**/*.ts' --- Place curly braces on separate lines for multi-line blocks: if (condition) { doSomething(); } else { doSomethingElse(); } ```` You can create instruction files with the **Chat: New Instructions File...** command. Moreover, the files created in the _user data_ folder can be automatically synchronized across multiple user machines through the Settings Sync service. Make sure to check the **Prompts and Instructions** option in the **Backup and Sync Settings...** dialog. Learn more about [instruction files](https://code.visualstudio.com/docs/copilot/copilot-customization#_instruction-files) in our documentation. ##### Prompt files **Setting**: `chat.promptFilesLocations` Prompt files describe a standalone, complete chat request, including the prompt text, chat mode, and tools to use. Prompt files are useful for creating reusable chat requests for common tasks. For example, you can add a prompt file for creating a front-end component, or to perform a security review. Prompt files use the `.prompt.md` file suffix. They can be located in your user data folder or in the workspace. The `chat.promptFilesLocations` setting lists the folder where prompt files are looked for. There are several ways to run a prompt file: * Type `/` in the chat input field, followed by the prompt file name. ![Screenshot that shows running a prompt in the Chat view with a slash command.](https://code.visualstudio.com/assets/updates/1_100/run-prompt-as-slash-command.png) * Open the prompt file in an editor and press the 'Play' button in the editor tool bar. This enables you to quickly iterate on the prompt and run it without having to switch back to the Chat view. ![Screenshot that shows running a prompt by using the play button in the editor.](https://code.visualstudio.com/assets/updates/1_100/run-prompt-from-play-button.png) * Use the **Chat: Run Prompt File...** command from the Command Palette. Prompt files can have the following Front Matter metadata headers to indicate how they should be run: * `mode`: the chat mode to use when invoking the prompt (`ask`, `edit`, or `agent` mode). * `tools`: if the `mode` is `agent`, the list of tools that are available for the prompt. The following example shows a prompt file for generating release notes, that runs in agent mode, and can use a set of tools: ```md --- mode: 'agent' tools: ['getCurrentMilestone', 'getReleaseFeatures', 'file_search', 'semantic_search', 'read_file', 'insert_edit_into_file', 'create_file', 'replace_string_in_file', 'fetch_webpage', 'vscode_search_extensions_internal'] --- Generate release notes for the features I worked in the current release and update them in the release notes file. Use [release notes writing instructions file](.github/instructions/release-notes-writing.instructions.md) as a guide. ``` To create a prompt file, use the **Chat: New Prompt File...** command from the Command Palette. Learn more about [prompt files](https://code.visualstudio.com/docs/copilot/copilot-customization#_prompt-files-experimental) in our documentation. ##### Improvements and notes * Instructions and prompt files now have their own language IDs, configurable in the _language mode_ dialog for any file open document ("Prompt" and "Instructions" respectively). This allows, for instance, using untitled documents as temporary prompt files before saving them as files to disk. * We renamed the **Chat: Use Prompt** command to **Chat: Run Prompt**. Furthermore, the command now runs the selected prompt _immediately_, as opposed to attaching it as chat context as it did before. * Both file types now also support the `description` metadata in their headers, providing a common place for short and user-friendly prompt summaries. In the future, this header is planned to be used along with the `applyTo` header as the rule that determines if the file needs to be auto-included with chat requests (for example, `description: 'Code style rules for front-end components written in TypeScript.'`) #### Faster agent mode edits with GPT 4.1 We've implemented support for OpenAI's apply patch editing format when using GPT 4.1 and o4-mini in agent mode. This means that you benefit from significantly faster edits, especially in large files. The tool is enabled by default in VS Code Insiders and will be progressively rolled out in VS Code Stable. #### Use GPT 4.1 as the base model When you're using chat in VS Code, the base model is now updated to GPT-4.1. You can still use the model switcher in the Chat view to change to another model. #### Search code of a GitHub repository with the `#githubRepo` tool Imagine you need to ask a question about a GitHub repository, but you don't have it open in your editor. You can now use the `#githubRepo` tool to search for code snippets in any GitHub repository that you have access to. This tool takes a `USER/REPO` and is a great way to quickly ask about a project you don't currently have open in VS Code. You can also use [custom instructions](https://code.visualstudio.com/docs/copilot/copilot-customization#_custom-instructions) to hint to Copilot when and how to use this tool: ```md --- applyTo: '**' --- Use the `#githubRepo` tool with `microsoft/vscode` to find relevant code snippets in the VS Code codebase. Use the `#githubRepo` tool with `microsoft/typescript` to answer questions about how TypeScript is implemented. ``` ![Screenshot showing using the #githubRepo tool in agent mode with hints from instructions files.](https://code.visualstudio.com/assets/updates/1_100/github-repo-tool-example.png) If you want to ask about the repo you are currently working on, you can just use the [`#codebase` tool](https://code.visualstudio.com/docs/copilot/reference/workspace-context#_making-copilot-chat-an-expert-in-your-workspace). Also, the `#githubRepo` tool is only for searching for relevant code snippets. The [GitHub MCP server](https://github.com/github/github-mcp-server?tab=readme-ov-file#github-mcp-server) provides tools for working with GitHub issues and pull requests. Learn more about [adding MCP servers in VS Code](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server). #### Find Marketplace extensions with the extensions tool Use `#extensions` tool to find extensions from the Marketplace. This tool is available in both chat and agent mode and is picked up automatically but you can also reference it explicitly via `#extensions` with your query. The tool returns a list of extensions that match your query, and you can install them directly from the results. #### Improvements to the web page fetch tool Last month, we introduced the `#fetch` tool, which allows you to fetch the contents of a web page right from chat to include as context for your prompt. If you missed that release note, check out [the initial release of the fetch tool](v1_99.md#fetch-tool) release note and examples. This iteration, we have made several big changes to the tool including: * **Entire page as context**: We now add the entire page as context, rather than a subset. With larger context windows, we have the ability to give the model the entire page. For example, it's now possible to ask summarization questions that require as much of the page as possible. If you _do_ manage to fill up the context window, the fetch tool is smart enough to exclude the less relevant sections of the page. That way, you don't exceed the context window limit, while still keeping the important parts. * **A standardized page format (Markdown)**: Previously, we formatted fetched webpages in a custom hierarchical format that did the job, but was sometimes hard to reason with because of its custom nature. We now convert fetched webpages into Markdown, a standardized language. This improves the reliability of the *relevancy detection* and is a format that most language models know deeply, so they can reason with it more easily. We'd love to hear how you use the fetch tool and if there are any capabilities you'd like to see from it! #### Chat input improvements We have made several improvements to the chat input box: * Attachments: when you reference context in the prompt text with `#`, they now also appear as an attachment pill. This makes it simpler to understand what's being sent to the language model. * Context picker: we streamlined the context picker to make it simpler to pick files, folders, and other attachment types. * Done button: we heard your feedback about the "Done"-button and we removed it! No more confusion about unexpected session endings. Now, we only start a new session when you create a new chat (Ctrl+L). #### Chat mode keyboard shortcuts The keyboard shortcut Ctrl+Alt+I still just opens the Chat view, but the Ctrl+Shift+I shortcut now opens the Chat view and switches to [agent mode](vscode://GitHub.Copilot-Chat/chat?mode=agent). If you'd like to set up keyboard shortcuts for other chat modes, there is a command for each mode: * `workbench.action.chat.openAgent` * `workbench.action.chat.openEdit` * `workbench.action.chat.openAsk` #### Autofix diagnostics from agent mode edits **Setting**: `github.copilot.chat.agent.autoFix` If a file edit in agent mode introduces new errors, agent mode can now detect them, and automatically propose a follow-up edit. You can disable this behavior with `github.copilot.chat.agent.autoFix`. #### Handling of undo and manual edits in agent mode Previously, making manual edits during an agent mode session could confuse the model. Now, the agent is prompted about your changes, and should re-read files when necessary before editing files that might have changed. #### Conversation history summarized and optimized for prompt caching We've made some changes to how our agent mode prompt is built to optimize for prompt caching. Prompt caching is a way to speed up model responses by maintaining a stable prefix for the prompt. The next request is able to resume from that prefix, and the result is that each request should be a bit faster. This is especially effective in a repetitive series of requests with large context, like you typically have in agent mode. When your conversation gets long, or your context gets very large, you might see a "Summarized conversation history" message in your agent mode session: ![Screenshot showing a summarized conversation message in the Chat view.](https://code.visualstudio.com/assets/updates/1_100/summarized-conversation.png) Instead of keeping the whole conversation as a FIFO, breaking the cache, we compress the conversation so far into a summary of the most important information and the current state of your task. This keeps the prompt prefix stable, and your responses fast. #### MCP support for Streamable HTTP This release adds support for the new Streamable HTTP transport for Model Context Protocol servers. Streamable HTTP servers are configured just like existing SSE servers, and our implementation is backwards-compatible with SSE servers: ```json { "servers": { "my-mcp-server": { "url": "http://localhost:3000/mcp" } } } ``` Learn more about [MCP support in VS Code](https://code.visualstudio.com/docs/copilot/chat/mcp-servers). #### MCP support for image output We now support MCP servers that generate images as part of their tool output. Note that not all language models support reading images from tool output. For example, although GPT-4.1 has vision capability, it does not currently support reading images from tools. #### Enhanced input, output, and progress from MCP servers We have enhanced the UI that shows MCP server tool input and output, and have also added support for MCP's new progress messages. _Theme: [Codesong](https://marketplace.visualstudio.com/items?itemName=connor4312.codesong) (preview on [vscode.dev](https://vscode.dev/editor/theme/connor4312.codesong))_ #### MCP config generation uses inputs To help keep your secrets secure, AI-assisted configurations generated by the **MCP: Add Server** command now generate `inputs` for any secrets, rather than inlining them into the resulting configuration. #### Inline chat V2 (Preview) **Setting**: `chat.inlineChat.enableV2:true` We have been working on a revamped version of inline chat Ctrl+I. Its theme is still "bringing chat into code", but behind the scenes it uses the same logic as chat edits. This means better use of the available context and a better code-editing strategy. You can enable inline chat v2 via `chat.inlineChat.enableV2:true` Further, there is now a more lightweight UX that can optionally be enabled. With the `chat.inlineChat.hideOnRequest:true` setting, inline chat hides as soon as a request is made. It then minimizes into the chat-editing overlay, which enables accepting or discarding changes, or restoring the inline chat control. #### Select and attach UI elements to chat (Experimental) **Setting**: `chat.sendElementsToChat.enabled` While you're developing a web application, you might want to ask chat about specific UI elements of a web page. You can now use the built-in Simple Browser to attach UI elements as context to chat. After opening any locally-hosted site via the built-in Simple Browser (launch it with the **Simple Browser: Show** command), a new toolbar is now shown where you can select **Start** to select any element in the site that you want. This attaches a screenshot of the selected element, and the HTML and CSS of the element. Configure what is attached to chat with: * `chat.sendElementsToChat.attachCSS`: enable or disable attaching the associated CSS * `chat.sendElementsToChat.attachImages`: enable or disable attaching the screenshot of the selected element This experimental feature is enabled by default for all Simple Browsers, but can be disabled with `chat.sendElementsToChat.enabled`. #### Create and launch tasks in agent mode (Experimental) **Setting**: `chat.newWorkspaceCreation.enabled` In the previous release, we introduced the `chat.newWorkspaceCreation.enabled` (Experimental) setting to enable workspace creation with agent mode. Now, at the end of this creation flow, you are prompted to create and run a task for launching your app or project. This streamlines the project launch process and enables easy task reuse. ### Configure VS Code #### Prevent installation of Copilot Chat pre-release versions in VS Code stable VS Code now prevents the installation of the pre-release version of the Copilot Chat extension in VS Code Stable. This helps avoid situations where you inadvertently install the Copilot Chat pre-release version and get stuck in a broken state. This means that you can only install the Copilot Chat extension pre-release version in the Insiders build of VS Code. #### Semantic text search with keyword suggestions (Experimental) **Setting**: `chat.search.keywordSuggestions:true` Semantic text search now supports AI-powered keyword suggestions. By enabling this feature, you will start seeing relevant references or definitions that might help you find the code you are looking for. ### Code Editing #### New Next Edit Suggestions (NES) model **Setting**: `github.copilot.nextEditSuggestions.enabled` We're excited to introduce a new model powering NES, designed to provide faster and more contextually relevant code recommendations. This updated model offers improved performance, delivering suggestions with reduced latency, and offering suggestions that are less intrusive and align more closely with your recent edits. This update is part of our ongoing commitment to refining AI-assisted development tools within Visual Studio Code. #### Import suggestions **Setting**: `github.copilot.nextEditSuggestions.fixes:true` Next Edit Suggestions (NES) can now automatically suggest adding missing import statements in JavaScript and TypeScript files. Enable this feature by setting `github.copilot.nextEditSuggestions.fixes:true`. We plan to further enhance this capability by supporting imports from additional languages in future updates. ![Screenshot showing NES suggesting an import statement.](https://code.visualstudio.com/assets/updates/1_100/nes-import.png) #### Generate alt text in HTML or Markdown You can now generate or update existing alt text in HTML and Markdown files. Navigate to any line containing an embedded image and trigger the quick fix via Ctrl+. or by selecting the lightbulb icon. ![Screenshot that shows generating alt text for an image html element.](https://code.visualstudio.com/assets/updates/1_100/generate-alt-text.png) ### Notebooks #### Drag and drop cell outputs to chat To enhance existing support for cell output usage within chat, outputs are now able to be dragged into the Chat view for a seamless attachment experience. Currently, only image and textual outputs are supported. Outputs with an image mime type are directly draggable, however to avoid clashing with text selection, textual outputs require holding the Alt modifier key to enable dragging. We are exploring UX improvements in the coming releases. #### Notebook tools for agent mode ##### Run cell Chat now has an LLM tool to run notebook cells, which allows the agent to perform updates based on cell run results or perform its own data exploration as it builds out a notebook. ##### Get kernel state The agent can find out which cells have been executed in the current kernel session, and read the active variables by using the Kernel State tool. ##### List/Install packages The Jupyter extension contributes tools for listing and installing packages into the environment that's being used as the notebook's kernel. The operation is delegated to the Python Environments extension if available; otherwise, it attempts to use the pip package manager. --- ## 0.26 (2025-04-02) GitHub Copilot updates from [March 2025](https://code.visualstudio.com/updates/v1_99): ### Accessibility #### Chat agent mode improvements You are now notified when manual action is required during a tool invocation, such as "Run command in terminal." This information is also included in the ARIA label for the relevant chat response, enhancing accessibility for screen reader users. Additionally, a new accessibility help dialog is available in [agent mode](https://code.visualstudio.com/docs/copilot/chat/chat-agent-mode), explaining what users can expect from the feature and how to navigate it effectively. #### Accessibility Signals for chat edit actions VS Code now provides auditory signals when you keep or undo AI-generated edits. These signals are configurable via `accessibility.signals.editsKept` and `accessibility.signals.editsUndone`. ### Configure the editor #### Unified chat experience We have streamlined the chat experience in VS Code into a single unified Chat view. Instead of having to move between separate views and lose the context of a conversation, you can now easily switch between the different chat modes. ![Screenshot that shows the chat mode picker in the Chat view.](https://code.visualstudio.com/assets/updates/1_99/chat-modes.png) Depending on your scenario, use either of these modes, and freely move mid-conversation: - Ask mode: optimized for asking questions about your codebase and brainstorming ideas. - Edit mode: optimized for making edits across multiple files in your codebase. - Agent mode: optimized for an autonomous coding flow, combining code edits and tool invocations. Get more details about the [unified chat view](#unified-chat-view). #### Faster workspace searches with instant indexing [Remote workspace indexes](https://code.visualstudio.com/docs/copilot/reference/workspace-context#remote-index) accelerate searching large codebases for relevant code snippets that AI uses while answering questions and generating edits. These remote indexes are especially useful for large codebases with tens or even hundreds of thousands of files. Previously, you'd have to press a button or run a command to build and start using a remote workspace index. With our new instant indexing support, we now automatically build the remote workspace index when you first try to ask a `#codebase`/`@workspace` question. In most cases, this remote index can be built in a few seconds. Once built, any codebase searches that you or anyone else working with that repo in VS Code makes will automatically use the remote index. Keep in mind that remote workspaces indexes are currently only available for code stored on GitHub. To use a remote workspace index, make sure your workspace contains a git project with a GitHub remote. You can use the [Copilot status menu](#copilot-status-menu) to see the type of index currently being used: ![Screenshot that shows the workspace index status in the Copilot Status Bar menu.](https://code.visualstudio.com/assets/updates/1_99/copilot-workspace-index-remote.png) To manage load, we are slowly rolling out instant indexing over the next few weeks, so you may not see it right away. You can still run the `GitHub Copilot: Build remote index command` command to start using a remote index when instant indexing is not yet enabled for you. #### Copilot status menu The Copilot status menu, accessible from the Status Bar, is now enabled for all users. This milestone we added some new features to it: - View [workspace index](https://code.visualstudio.com/docs/copilot/reference/workspace-context) status information at any time. ![Screenshot that shows the workspace index status of a workspace in the Copilot menu.](https://code.visualstudio.com/assets/updates/1_99/copilot-worksspace-index-local-status.png) - View if code completions are enabled for the active editor. A new icon reflects the status, so that you can quickly see if code completions are enabled or not. ![Screenshot that shows the Copilot status icon when completions is disabled.](https://code.visualstudio.com/assets/updates/1_99/copilot-disabled-status.png) - Enable or disable [code completions and NES](https://code.visualstudio.com/docs/copilot/ai-powered-suggestions). #### Out of the box Copilot setup (Experimental) **Setting**: `chat.setupFromDialog` We are shipping an experimental feature to show functional chat experiences out of the box. This includes the Chat view, editor/terminal inline chat, and quick chat. The first time you send a chat request, we will guide you through signing in and signing up for Copilot Free. If you want to see this experience for yourself, enable the `chat.setupFromDialog` setting. #### Chat prerelease channel mismatch If you have the prerelease version of the Copilot Chat extension installed in VS Code Stable, a new welcome screen will inform you that this configuration is not supported. Due to rapid development of chat features, the extension will not activate in VS Code Stable. The welcome screen provides options to either switch to the release version of the extension or download [VS Code Insiders](https://code.visualstudio.com/insiders/). ![Screenshot that shows the welcome view of chat, indicating that the pre-release version of the extension is not supported in VS Code stable. A button is shown to switch to the release version, and a secondary link is shown to switch to VS Code Insiders.](https://code.visualstudio.com/assets/updates/1_99/welcome-pre-release.png) #### Semantic text search improvements (Experimental) **Setting**: `github.copilot.chat.search.semanticTextResults:true` AI-powered semantic text search is now enabled by default in the Search view. Use the Ctrl+I keyboard shortcut to trigger a semantic search, which shows you the most relevant results based on your query, on top of the regular search results. You can also reference the semantic search results in your chat prompt by using the `#searchResults` tool. This allows you to ask the LLM to summarize or explain the results, or even generate code based on them. ### Code Editing #### Agent mode is available in VS Code Stable **Setting**: `chat.agent.enabled:true` We're happy to announce that agent mode is available in VS Code Stable! Enable it by setting `chat.agent.enabled:true`. Enabling the setting will no longer be needed in the following weeks, as we roll out enablement by default to all users. Check out the [agent mode documentation](https://code.visualstudio.com/docs/copilot/chat/chat-agent-mode) or select agent mode from the chat mode picker in the Chat view. ![Screenshot that shows the Chat view, highlighting agent mode selected in the chat mode picker.](https://code.visualstudio.com/assets/updates/1_99/copilot-edits-agent-mode.png) #### AI edits improvements We have done some smaller tweaks when generating edits with AI: * Mute diagnostics events outside the editor while rewriting a file with AI edits. Previously, we already disabled squiggles in this scenario. These changes reduce flicker in the Problems panel and also ensure that we don't issue requests for the quick fix code actions. * We now explicitly save a file when you decide to keep the AI edits. #### Next Edit Suggestions general availability **Setting**: `github.copilot.nextEditSuggestions.enabled:true` We're happy to announce the general availability of Next Edit Suggestions (NES)! In addition, we've also made several improvements to the overall user experience of NES: * Make edit suggestions more compact, less interfering with surrounding code, and easier to read at a glance. * Updates to the gutter indicator to make sure that all suggestions are more easily noticeable. #### Improved edit mode **Setting**: `chat.edits2.enabled:true` We're making a change to the way [edit mode in chat](https://code.visualstudio.com/docs/copilot/chat/copilot-edits) operates. The new edit mode uses the same approach as agent mode, where it lets the model call a tool to make edits to files. An upside to this alignment is that it enables you to switch seamlessly between all three modes, while providing a huge simplification to how these modes work under the hood. A downside is that this means that the new mode only works with the same reduced set of models that agent mode works with, namely models that support tool calling and have been tested to be sure that we can have a good experience when tools are involved. You may notice models like `o3-mini` and `Claude 3.7 (Thinking)` missing from the list in edit mode. If you'd like to keep using those models for editing, disable the `chat.edits2.enabled` setting to revert to the previous edit mode. You'll be asked to clear the session when switching modes. We've learned that prompting to get consistent results across different models is harder when using tools, but we are working on getting these models lit up for edit (and agent) modes. This setting will be enabled gradually for users in VS Code Stable. #### Inline suggestion syntax highlighting **Setting**: `editor.inlineSuggest.syntaxHighlightingEnabled` With this update, syntax highlighting for inline suggestions is now enabled by default. Notice in the following screenshot that the code suggestion has syntax coloring applied to it. ![Screenshot of the editor, showing that syntax highlighting is enabled for ghost text.](https://code.visualstudio.com/assets/updates/1_99/inlineSuggestionHighlightingEnabled.png) If you prefer inline suggestions without syntax highlighting, you can disable it with `editor.inlineSuggest.syntaxHighlightingEnabled:false`. ![Screenshot of the editor showing that highlighting for ghost text is turned off.](https://code.visualstudio.com/assets/updates/1_99/inlineSuggestionHighlightingDisabled.png) ### Chat #### Model Context Protocol server support This release supports [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) servers in agent mode. Once configured in VS Code, MCP servers provide tools for agent mode to interact with other systems, such as databases, cloud platforms, search engines, or any 3rd party API. MCP servers can be configured under the `mcp` section in your user, remote, or `.code-workspace` settings, or in `.vscode/mcp.json` in your workspace. The configuration supports input variables to avoid hard-coding secrets and constants. For example, you can use `${env:API_KEY}` to reference an environment variable or `${input:ENDPOINT}` to prompt for a value when the server is started. You can use the **MCP: Add Server** command to quickly set up an MCP server from a command line invocation, or use an AI-assisted setup from an MCP server published to Docker, npm, or PyPI. When a new MCP server is added, a refresh action is shown in the Chat view, which can be used to start the server and discover the tools. Afterwards, servers are started on-demand to save resources. _Theme: [Codesong](https://marketplace.visualstudio.com/items?itemName=connor4312.codesong) (preview on [vscode.dev](https://vscode.dev/editor/theme/connor4312.codesong))_ If you've already been using MCP servers in other applications such as Claude Desktop, VS Code will discover them and offer to run them for you. This behavior can be toggled with the setting `chat.mcp.discovery.enabled`. You can see the list of MCP servers and their current status using the **MCP: List Servers** command, and pick the tools available for use in chat by using the **Select Tools** button in agent mode. You can read more about how to install and use MCP servers in [our documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers). #### Making agent mode available in VS Code Stable We're happy to announce that agent mode is available in VS Code Stable! Enable it by setting `chat.agent.enabled:true`. Enabling the setting will no longer be needed in the following weeks, as we roll out enablement by default to all users. Check out the [agent mode documentation](https://code.visualstudio.com/docs/copilot/chat/chat-agent-mode) or select agent mode from the chat mode picker in the Chat view. ![Screenshot that shows the Chat view, highlighting agent mode selected in the chat mode picker.](https://code.visualstudio.com/assets/updates/1_99/copilot-edits-agent-mode.png) #### Agent mode tools This milestone, we have added several new built-in tools to agent mode. ##### Thinking tool **Setting**: `github.copilot.chat.agent.thinkingTool:true`. Inspired by [Anthropic's research](https://www.anthropic.com/engineering/claude-think-tool), we've added support for a thinking tool in agent mode that can be used to give any model the opportunity to think between tool calls. This improves our agent's performance on complex tasks in-product and on the [SWE-bench](https://www.swebench.com/) eval. ##### Fetch tool Use the `#fetch` tool for including content from a publicly accessible webpage in your prompt. For instance, if you wanted to include the latest documentation on a topic like [MCP](#model-context-protocol-server-support), you can ask to fetch [the full documentation](https://modelcontextprotocol.io/llms-full.txt) (which is conveniently ready for an LLM to consume) and use that in a prompt. Here's a video of what that might look like: In agent mode, this tool is picked up automatically but you can also reference it explicitly in the other modes via `#fetch`, along with the URL you are looking to fetch. This tool works by rendering the webpage in a headless browser window in which the data of that page is cached locally, so you can freely ask the model to fetch the contents over and over again without the overhead of re-rendering. Let us know how you use the `#fetch` tool, and what features you'd like to see from it! **Fetch tool limitations:** * Currently, JavaScript is disabled in this browser window. The tool will not be able to acquire much context if the website depends entirely on JavaScript to render content. This is a limitation we are considering changing and likely will change to allow JavaScript. * Due to the headless nature, we are unable to fetch pages that are behind authentication, as this headless browser exists in a different browser context than the browser you use. Instead, consider using [MCP](#model-context-protocol-server-support) to bring in an MCP server that is purpose-built for that target, or a generic browser MCP server such as the [Playwright MCP server](https://github.com/microsoft/playwright-mcp). ##### Usages tool The `#usages` tool is a combination of "Find All References", "Find Implementation", and "Go to Definition". This tool can help chat to learn more about a function, class, or interface. For instance, chat can use this tool to look for sample implementations of an interface or to find all places that need to be changed when making a refactoring. In agent mode this tool will be picked up automatically but you can also reference it explicitly via `#usages` #### Create a new workspace with agent mode (Experimental) **Setting**: `github.copilot.chat.newWorkspaceCreation.enabled` You can now scaffold a new VS Code workspace in [agent mode](https://code.visualstudio.com/docs/copilot/chat/chat-agent-mode). Whether you’re setting up a VS Code extension, an MCP server, or other development environments, agent mode helps you to initialize, configure, and launch these projects with the necessary dependencies and settings. #### VS Code extension tools in agent mode Several months ago, we finalized our extension API for [language model tools](https://code.visualstudio.com/api/extension-guides/tools#create-a-language-model-tool) contributed by VS Code extensions. Now, you can use these tools in agent mode. Any tool contributed to this API which sets `toolReferenceName` and `canBeReferencedInPrompt` in its configuration is automatically available in agent mode. By contributing a tool in an extension, it has access to the full VS Code extension APIs, and can be easily installed via the Extension Marketplace. Similar to tools from MCP servers, you can enable and disable these with the **Select Tools** button in agent mode. See our [language model tools extension guide](https://code.visualstudio.com/api/extension-guides/tools#create-a-language-model-tool) to build your own! #### Agent mode tool approvals As part of completing the tasks for a user prompt, agent mode can run tools and terminal commands. This is powerful but potentially comes with risks. Therefore, you need to approve the use of tools and terminal commands in agent mode. To optimize this experience, you can now remember that approval on a session, workspace, or application level. This is not currently enabled for the terminal tool, but we plan to develop an approval system for the terminal in future releases. ![Screenshot that shows the agent mode tool Continue button dropdown options for remembering approval.](https://code.visualstudio.com/assets/updates/1_99/chat-tool-approval.png) In case you want to auto-approve _all_ tools, you can now use the experimental `chat.tools.autoApprove:true` setting. This will auto-approve all tools, and VS Code will not ask for confirmation when a language model wishes to run tools. Bear in mind that with this setting enabled, you will not have the opportunity to cancel potentially destructive actions a model wants to take. We plan to expand this setting with more granular capabilities in the future. #### Agent evaluation on SWE-bench VS Code's agent achieves a pass rate of 56.0% on `swebench-verified` with Claude 3.7 Sonnet, following Anthropic's [research](https://www.anthropic.com/engineering/swe-bench-sonnet) on configuring agents to execute without user input in the SWE-bench environment. Our experiments have translated into shipping improved prompts, tool descriptions and tool design for agent mode, including new tools for file edits that are in-distribution for Claude 3.5 and 3.7 Sonnet models. #### Unified Chat view For the past several months, we've had a "Chat" view for asking questions to the language model, and a "Copilot Edits" view for an AI-powered code editing session. This month, we aim to streamline the chat-based experience by merging the two views into one Chat view. In the Chat view, you'll see a dropdown with three modes: ![Screenshot that shows the chat mode picker in the Chat view.](https://code.visualstudio.com/assets/updates/1_99/chat-modes.png) - **[Ask](https://code.visualstudio.com/docs/copilot/chat/chat-ask-mode)**: This is the same as the previous Chat view. Ask questions about your workspace or coding in general, using any model. Use `@` to invoke built-in chat participants or from installed [extensions](https://marketplace.visualstudio.com/search?term=chat-participant&target=VSCode&category=All%20categories&sortBy=Relevance). Use `#` to attach any kind of context manually. - **[Agent](https://code.visualstudio.com/docs/copilot/chat/chat-agent-mode)**: Start an agentic coding flow with a set of tools that let it autonomously collect context, run terminal commands, or take other actions to complete a task. Agent mode is enabled for all [VS Code Insiders](https://code.visualstudio.com/insiders/) users, and we are rolling it out to more and more users in VS Code Stable. - **[Edit](https://code.visualstudio.com/docs/copilot/chat/copilot-edits)**: In Edit mode, the model can make directed edits to multiple files. Attach `#codebase` to let it find the files to edit automatically. But it won't run terminal commands or do anything else automatically. > **Note**: If you don't see agent mode in this list, then either it has not yet been enabled for you, or it's disabled by organization policy and needs to be enabled by the [organization owner](https://aka.ms/github-copilot-org-enable-features). Besides making your chat experience simpler, this unification enables a few new features for AI-powered code editing: - **Switch modes in the middle of a conversation**: For example, you might start brainstorming an app idea in ask mode, then switch to agent mode to execute the plan. Tip: press Ctrl+. to change modes quickly. - **Edit sessions in history**: Use the **Show Chats** command (clock icon at the top of the Chat view) to restore past edit sessions and keep working on them. - **Move chat to editor or window**: Select **Open Chat in New Editor/New Window** to pop out your chat conversation from the side bar into a new editor tab or separate VS Code window. Chat has supported this for a long time, but now you can run your edit/agent sessions from an editor pane or a separate window as well. - **Multiple agent sessions**: Following from the above point, this means that you can even run multiple agent sessions at the same time. You might like to have one chat in agent mode working on implementing a feature, and another independent session for doing research and using other tools. Directing two agent sessions to edit files at the same time is not recommended, it can lead to confusion. #### Bring Your Own Key (BYOK) (Preview) Copilot Pro and Copilot Free users can now bring their own API keys for popular providers such as Azure, Anthropic, Gemini, Open AI, Ollama, and Open Router. This allows you to use new models that are not natively supported by Copilot the very first day that they're released. To try it, select **Manage Models...** from the model picker. We’re actively exploring support for Copilot Business and Enterprise customers and will share updates in future releases. To learn more about this feature, head over to our [docs](https://code.visualstudio.com/docs/copilot/language-models). ![A screenshot of a "Manage Models - Preview" dropdown menu in a user interface. The dropdown has the label "Select a provider" at the top, with a list of options below it. The options include "Anthropic" (highlighted in blue), "Azure," "Gemini," "OpenAI," "Ollama," and "OpenRouter." A gear icon is displayed next to the "Anthropic" option.](https://code.visualstudio.com/assets/updates/1_99/byok.png) #### Reusable prompt files ##### Improved configuration **Setting**: `chat.promptFilesLocations` The `chat.promptFilesLocations` setting now supports glob patterns in file paths. For example, to include all `.prompt.md` files in the currently open workspace, you can set the path to `{ "**": true }`. Additionally, the configuration now respects case sensitivity on filesystems where it applies, aligning with the behavior of the host operating system. ##### Improved editing experience - Your `.prompt.md` files now offer basic autocompletion for filesystem paths and highlight valid file references. Broken links on the other hand now appear as warning or error squiggles and provide detailed diagnostic information. - You can now manage prompts using edit and delete actions in the prompt file list within the **Chat: Use Prompt** command. - Folder references in prompt files are no longer flagged as invalid. - Markdown comments are now properly handled, for instance, all commented-out links are ignored when generating the final prompt sent to the LLM model. ##### Alignment with custom instructions The `.github/copilot-instructions.md` file now behaves like any other reusable `.prompt.md` file, with support for nested link resolution and enhanced language features. Furthermore, any `.prompt.md` file can now be referenced and is handled appropriately. Learn more about [custom instructions](https://code.visualstudio.com/docs/copilot/copilot-customization). ##### User prompts The **Create User Prompt** command now allows creating a new type of prompts called _user prompts_. These are stored in the user data folder and can be synchronized across machines, similar to code snippets or user settings. The synchronization can be configured in [Sync Settings](https://code.visualstudio.com/docs/configure/settings-sync) by using the **Prompts** item in the synchronization resources list. #### Improved vision support (Preview) Last iteration, Copilot Vision was enabled for `GPT-4o`. Check our [release notes](https://code.visualstudio.com/updates/v1_98#_copilot-vision-preview) to learn more about how you can attach and use images in chat. This release, you can attach images from any browser via drag and drop. Images drag and dropped from browsers must have the correct url extension, with `.jpg`, `.png`, `.gif`, `.webp`, or `.bmp`. ### Notebooks #### AI notebook editing improvements AI-powered editing support for notebooks (including agent mode) is now available in the Stable release. This was added last month as a preview feature in [VS Code Insiders](https://code.visualstudio.com/insiders). You can now use chat to edit notebook files with the same intuitive experience as editing code files: modify content across multiple cells, insert and delete cells, and change cell types. This feature provides a seamless workflow when working with data science or documentation notebooks. ##### New notebook tool VS Code now provides a dedicated tool for creating new Jupyter notebooks directly from chat. This tool plans and creates a new notebook based on your query. Use the new notebook tool in agent mode or edit mode (make sure to enable the improved edit mode with `chat.edits2.enabled:true)`. If you're using ask mode, type `/newNotebook` in the chat prompt to create a new notebook. ##### Navigate through AI edits Use the diff toolbars to iterate through and review each AI edit across cells. ##### Undo AI edits When focused on a cell container, the **Undo** command reverts the full set of AI changes at the notebook level. ##### Text and image output support in chat You can now add notebook cell outputs, such as text, errors, and images, directly to chat as context. This lets you reference the output when using ask, edit, or agent mode, making it easier for the language model to understand and assist with your notebook content. Use the **Add cell output to chat** action, available via the triple-dot menu or by right-clicking the output. To attach the cell error output as chat context: To attach the cell output image as chat context: ### Terminal #### Reliability in agent mode The tool that allows agent mode to run commands in the terminal has a number of reliability and compatibility improvements. You should expect fewer cases where the tool gets stuck or where the command finishes without the output being present. One of the bigger changes is the introduction of the concept of "rich" quality [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration), as opposed to "basic" and "none". The shell integration scripts shipped with VS Code should generally all enable rich shell integration which provides the best experience in the run in terminal tool (and terminal usage in general). You can view the shell integration quality by hovering over the terminal tab. --- ## 0.25 (2025-03-05) GitHub Copilot updates from [February 2025](https://code.visualstudio.com/updates/v1_98): ### Copilot Edits #### Agent mode improvements (Experimental) Last month, we introduced _agent mode_ for Copilot Edits in [VS Code Insiders](https://code.visualstudio.com/insiders/). In agent mode, Copilot can automatically search your workspace for relevant context, edit files, check them for errors, and run terminal commands (with your permission) to complete a task end-to-end. > **Note**: Agent mode is available today in [VS Code Insiders](https://code.visualstudio.com/insiders/), and we just started rolling it out gradually in **VS Code Stable**. Once agent mode is enabled for you, you will see a mode dropdown in the Copilot Edits view — simply select **Agent**. We made several improvements to the UX of tool usages this month: * Terminal commands are now shown inline, so you can keep track of which commands were run. * You can edit the suggested terminal command in the chat response before running it. * Confirm a terminal command with the Ctrl+Enter shortcut. Agent mode autonomously searches your codebase for relevant context. Expand the message to see the results of which searches were done. ![Screenshot that shows the expandable list of search results in Copilot Edits.](https://code.visualstudio.com/assets/updates/1_98/agent-mode-search-results.png) We've also made various improvements to the prompt and behavior of agent mode: * The undo and redo actions in chat now undo or redo the last file edit made in a chat response. This is useful for agent mode, as you can now undo certain steps the model took without rolling back the entire chat response. * Agent mode can now run your build [tasks](https://code.visualstudio.com/docs/editor/tasks) automatically or when instructed to do so. Disable this functionality via the `github.copilot.chat.agent.runTasks` setting, in the event that you see the model running tasks when it should not. Learn more about [Copilot Edits agent mode](https://code.visualstudio.com/docs/copilot/copilot-edits#_use-agent-mode-preview) or read the [agent mode announcement blog post](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode). > **Note**: If you are a Copilot Business or Enterprise user, an administrator of your organization [must opt in](https://docs.github.com/en/copilot/managing-copilot/managing-github-copilot-in-your-organization/managing-policies-for-copilot-in-your-organization#enabling-copilot-features-in-your-organization) to the use of Copilot "Editor Preview Features" for agent mode to be available. #### Notebook support in Copilot Edits (Preview) We are introducing notebook support in Copilot Edits. You can now use Copilot to edit notebook files with the same intuitive experience as editing code files. Create new notebooks from scratch, modify content across multiple cells, insert and delete cells, and change cell types. This preview feature provides a seamless workflow when working with data science or documentation notebooks. > For the best notebook editing experience with Copilot, we recommend using [VS Code Insiders](https://code.visualstudio.com/insiders/) and the pre-release version of GitHub Copilot Chat, where you'll get the latest improvements to this feature as they're developed. #### Refined editor integration We have polished the integration of Copilot Edits with code and notebook editors: * No more scrolling while changes are being applied. The viewport remains in place, making it easier to focus on what changes. * Renamed the edit review actions from "Accept" to "Keep" and "Discard" to "Undo" to better reflect what’s happening. Changes for Copilot Edits are live, they are applied and saved as they are made and users keep or undo them. * After keeping or undoing a file, the next file is automatically revealed. The video demonstrates how edits are applied and saved as they occur. The live preview updates, and the user decided to "Keep" the changes. Undoing and further tweaking is also still possible. #### Refreshed UI In preparation for unifying Copilot Edits with Copilot Chat, we've given Copilot Edits a facelift. Files that are attached and not yet sent, are now rendered as regular chat attachments. Only files that have been modified with AI are added to the changed files list, which is located above the chat input part. With the `chat.renderRelatedFiles` setting, you can enable getting suggestions for related files. Related file suggestions are rendered below the chat attachments. ![Screenshot that shows the updated Copilot Edits attachments and changed files user experience.](https://code.visualstudio.com/assets/updates/1_98/copilot_edits_ui.png) ### Removed Copilot Edits limits Previously, you were limited to attach 10 files to your prompt in Copilot Edits. With this release, we removed this limit. Additionally, we've removed the client-side rate limit of 14 interactions per 10 minutes. > Note that service-side usage rate limits still apply. ### Smoother authentication flows in chat If you host your source code in a GitHub repository, you're able to leverage several features, including advanced code searching, the `@github` chat participant, and more! However, for private GitHub repositories, VS Code needs to have permission to interact with your repositories on GitHub. For a while, this was presented with our usual VS Code authentication flow, where a modal dialog showed up when you invoked certain functionality (for example, asking `@workspace` or `@github` a question, or using the `#codebase` tool). To make this experience smoother, we've introduced this confirmation in chat: ![Screenshot that shows the authentication confirmation dialog in Chat, showing the three options to continue.](https://code.visualstudio.com/assets/updates/1_98/confirmation-auth-dialog.png) Not only is it not as jarring as a modal dialog, but it also has new functionality: 1. **Grant:** you're taken through the regular authentication flow like before (via the modal). 1. **Not Now:** VS Code remembers your choice and won't bother you again until your next VS Code window session. The only exception to this is if the feature needs this additional permission to function, like `@github`. 1. **Never Ask Again:** VS Code remembers your choice and persists it via the `github.copilot.advanced.authPermissions` setting. Any feature that needs this additional permission will fail. It's important to note that this confirmation does not confirm or deny Copilot (the service) access to your repositories. This is only how VS Code's Copilot experience authenticates. To configure what Copilot can access, please read the docs [on content exclusion](https://docs.github.com/en/copilot/managing-copilot/configuring-and-auditing-content-exclusion/excluding-content-from-github-copilot). ### More advanced codebase search in Copilot Chat **Setting**: `github.copilot.chat.codesearch.enabled` When you add `#codebase` to your Copilot Chat query, Copilot helps you find relevant code in your workspace for your chat prompt. `#codebase` can now run tools like text search and file search to pull in additional context from your workspace. Set `github.copilot.chat.codesearch.enabled` to enable this behavior. The full list of tools is: * Embeddings-based semantic search * Text search * File search * Git modified files * Project structure * Read file * Read directory * Workspace symbol search ### Attach problems as chat context To help with fixing code or other issues in your workspace, you can now attach problems from the Problems panel to your chat as context for your prompt. Either drag an item from the Problems panel onto the Chat view, type `#problems` in your prompt, or select the paperclip 📎 button. You can attach specific problems, all problems in a file, or all files in your codebase. ### Attach folders as context Previously, you could attach folders as context by using drag and drop from the Explorer view. Now, you can also attach a folder by selecting the paperclip 📎 icon or by typing `#folder:` followed by the folder name in your chat prompt. ### Collapsed mode for Next Edit Suggestions (Preview) **Settings**: * `github.copilot.nextEditSuggestions.enabled` * `editor.inlineSuggest.edits.showCollapsed:true` We've added a collapsed mode for NES. When you enable this mode, only the NES suggestion indicator is shown in the left editor margin. The code suggestion itself is revealed only when you navigate to it by pressing Tab. Consecutive suggestions are shown immediately until a suggestion is not accepted. The collapsed mode is disabled by default and can be enabled by configuring `editor.inlineSuggest.edits.showCollapsed:true`, or you can enable or disable it in the NES gutter indicator menu. ![Screenshot that shows the Next Edit Suggestions context menu in the editor left margin, highlighting the Show Collapsed option.](https://code.visualstudio.com/assets/updates/1_98/NESgutterMenu.png) ### Change completions model You could already change the language model for Copilot Chat and Copilot Edits, and now you can also change the model for inline suggestions. Alternatively, you can change the model that is used for code completions via **Change Completions Model** command in the Command Palette or the **Configure Code Completions** item in the Copilot menu in the title bar. > **Note:** the list of available models might vary and change over time. If you are a Copilot Business or Enterprise user, your Administrator needs to enable certain models for your organization by opting in to `Editor Preview Features` in the [Copilot policy settings](https://docs.github.com/en/enterprise-cloud@latest/copilot/managing-copilot/managing-github-copilot-in-your-organization/managing-policies-for-copilot-in-your-organization#enabling-copilot-features-in-your-organization) on GitHub.com. ### Model availability This release, we added more models to choose from when using Copilot. The following models are now available in the model picker in Visual Studio Code and github.com chat: * **GPT 4.5 (Preview)**: OpenAI’s latest model, GPT-4.5, is now available in GitHub Copilot Chat to Copilot Enterprise users. GPT-4.5 is a large language model designed with advanced capabilities in intuition, writing style, and broad knowledge. Learn more about the GPT-4.5 model availability in the [GitHub blog post](https://github.blog/changelog/2025-02-27-openai-gpt-4-5-in-github-copilot-now-available-in-public-preview). * **Claude 3.7 Sonnet (Preview)**: Claude 3.7 Sonnet is now available to all customers on paid Copilot plans. This new Sonnet model supports both thinking and non-thinking modes in Copilot. In initial testing, we’ve seen particularly strong improvements in agentic scenarios. Learn more about the Claude 3.7 Sonnet model availability in the [GitHub blog post](https://github.blog/changelog/2025-02-24-claude-3-7-sonnet-is-now-available-in-github-copilot-in-public-preview/). ### Copilot Vision (Preview) We're quickly rolling out end-to-end vision support in this version of Copilot Chat. This lets you attach images and interact with images in chat prompts. For example, if you encounter an error while debugging, attach a screenshot of VS Code, and ask Copilot to help you resolve the issue. You might also use it to attach some UI mockup and let Copilot provide some HTML and CSS to implement the mockup. ![Animation that shows an attached image in a Copilot Chat prompt. Hovering over the image shows a preview of it.](https://code.visualstudio.com/assets/updates/1_97/image-attachments.gif) You can attach images in multiple ways: * Drag and drop images from your OS or from the Explorer view * Paste an image from your clipboard * Attach a screenshot of the VS Code window (select the **paperclip 📎 button** > **Screenshot Window**) A warning is shown if the selected model currently does not have the capability to handle the file type. The only supported model at the moment will be `GPT 4o`, but support for image attachments with `Claude 3.5 Sonnet` and `Gemini 2.0 Flash` will be rolling out soon as well. Currently, the supported image types are `JPEG/JPG`, `PNG`, `GIF`, and `WEBP`. ### Copilot status overview (Experimental) **Setting**: `chat.experimental.statusIndicator.enabled` We are experimenting with a new central Copilot status overview, accessible via the Status Bar. This view shows: * Quota information if you are a [Copilot Free](https://code.visualstudio.com/blogs/2024/12/18/free-github-copilot) user * Editor related settings such as Code Completions * Useful keyboard shortcuts to use other Copilot features You can enable this new Status Bar entry by configuring the new `chat.experimental.statusIndicator.enabled` setting. ### TypeScript context for inline completions (Experimental) **Setting**: `chat.languageContext.typescript.enabled` We are experimenting with enhanced context for inline completions and `/fix` commands for TypeScript files. The experiment is currently scoped to Insider releases and can be enabled with the `chat.languageContext.typescript.enabled` setting. ### Custom instructions for pull request title and description You can provide custom instructions for generating pull request title and description with the setting `github.copilot.chat.pullRequestDescriptionGeneration.instructions`. You can point the setting to a file in your workspace, or you can provide instructions inline in your settings: ``` { "github.copilot.chat.pullRequestDescriptionGeneration.instructions": [ { "text": "Prefix every PR title with an emoji." } ] } ``` Generating a title and description requires the GitHub Pull Requests extension to be installed. ## Previous release: https://code.visualstudio.com/updates ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Microsoft Open Source Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). Resources: - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns - Employees can reach out at [aka.ms/opensource/moderation-support](https://aka.ms/opensource/moderation-support) ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to GitHub Copilot Chat * [Creating good issues](#creating-good-issues) * [Look For an Existing Issue](#look-for-an-existing-issue) * [Writing Good Bug Reports and Feature Requests](#writing-good-bug-reports-and-feature-requests) * [Developing](#developing) * [Requirements](#requirements) * [First-time setup](#first-time-setup) * [Testing](#testing) * [Use base/common utils](#use-basecommon-utils) * [Developing Prompts](#developing-prompts) * [Motivations for TSX prompt crafting](#motivations-for-tsx-prompt-crafting) * [Quickstart](#quickstart) * [Code structure](#code-structure) * [Project Architecture and Coding Standards](#project-architecture-and-coding-standards) * [Layers](#layers) * [Runtimes (node.js, web worker)](#runtimes-nodejs-web-worker) * [Contributions and Services](#contributions-and-services) * [Agent mode](#agent-mode) * [Tools](#tools) * [Developing tools](#developing-tools) * [Tree Sitter](#tree-sitter) * [Troubleshooting](#troubleshooting) * [Reading requests](#reading-requests) * [API updates](#api-updates) * [Making breaking changes to API](#making-breaking-changes-to-api) * [Making additive changes to API](#making-additive-changes-to-api) * [Running with Code OSS](#running-with-code-oss) # Creating good issues ## Look For an Existing Issue Before you create a new issue, please do a search in [open issues](https://github.com/microsoft/vscode/issues) to see if the issue or feature request has already been filed. Be sure to scan through the [most popular](https://github.com/microsoft/vscode/issues?q=is%3Aopen+is%3Aissue+label%3Afeature-request+sort%3Areactions-%2B1-desc) feature requests. If you find your issue already exists, make relevant comments and add your [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments). Use a reaction in place of a "+1" comment: * 👍 - upvote * 👎 - downvote If you cannot find an existing issue that describes your bug or feature, create a new issue using the guidelines below. ### Writing Good Bug Reports and Feature Requests File a single issue per problem and feature request. Do not enumerate multiple bugs or feature requests in the same issue. Do not add your issue as a comment to an existing issue unless it's for the identical input. Many issues look similar but have different causes. The more information you can provide, the more likely someone will be successful at reproducing the issue and finding a fix. The built-in tool for reporting an issue, which you can access by using `Report Issue` in VS Code's Help menu, can help streamline this process by automatically providing the version of VS Code, all your installed extensions, and your system info. Additionally, the tool will search among existing issues to see if a similar issue already exists. Please include the following with each issue: * Version of VS Code and copilot-chat extension * Your operating system * The LLM model if applicable * Reproducible steps (1... 2... 3...) that cause the issue * What you expected to see, versus what you actually saw * Images, animations, or a link to a video showing the issue occurring * A code snippet or prompt that demonstrates the issue or a link to a code repository the developers can easily pull down to recreate the issue locally * **Note:** Because the developers need to copy and paste the code snippet, including a code snippet as a media file (i.e. .gif) is not sufficient. * Errors from the Dev Tools Console (open from the menu: Help > Toggle Developer Tools) # Developing ## Requirements - Node 22.x - Python >= 3.10, <= 3.12 - Git Large File Storage (LFS) - for running tests - (Windows) Visual Studio Build Tools >=2019 - for building with node-gyp [see node-gyp docs](https://github.com/nodejs/node-gyp?tab=readme-ov-file#on-windows) ### First-time setup - On Windows you need to run `Set-ExecutionPolicy Unrestricted` as admin in Powershell. - `npm install` - `npm run get_token` - Then you can run the build task with `Cmd+Shift+B` (or `Ctrl+Shift+B` if you are on Windows), or just start the "Launch Copilot Extension - Watch Mode" launch config to start the build then start debugging the extension. **Tip:** If "Launch Copilot Extension - Watch Mode" doesn't work for you, try using the "Launch Copilot Extension" debug configuration instead. **Note:** Setup and running under Windows Subsystem for Linux (WSL) is supported by following the [VS Code setup instructions](https://github.com/microsoft/vscode/wiki/Selfhosting-on-Windows-WSL). ### Testing If you hit errors while running tests, ensure that you are using the correct Node version and that git lfs is properly installed (run `git lfs pull` to validate). There are unit tests which run in Node.JS: ``` npm run test:unit ``` There are also integration tests that run within VS Code itself: ``` npm run test:extension ``` Finally, there are **simulation tests**. These tests reach out to Copilot API endpoints, invoke LLMs and require expensive computations to run. Each test runs 10 times, to accommodate for the stochastic nature of LLMs themselves. The results of all runs of all tests are snapshotted in the baseline file, [`test/simulation/baseline.json`](test/simulation/baseline.json), which encodes the quality of the test suite at any given point in time. Because LLM results are both random and costly, they are cached within the repo in `test/simulation/cache`. This means rerunning the simulation tests and benefiting from the cache will make the test run be both faster as well as deterministic. You can run the simulation tests with: ``` npm run simulate ``` Keep in mind that PRs will fail unless the cache is populated. Running the command above will populate the cache by creating new cache layers in `test/simulation/cache/layers`. This cache population must be done by VS Code team members. If a community member submits a PR with new cache layer(s), the PR will fail and a VS Code team member must delete the layer(s) and recreate them within their dev box. You can ensure the cache is populated with: ``` npm run simulate-require-cache ``` Finally, the PR will also fail with any uncommitted baseline changes. If you do see change test results locally, and would like to accept the new baseline for the simulation tests, you should update the baseline and include that change in your commit: ``` npm run simulate-update-baseline ``` ### Use `base/common` utils We like and miss our utilities from the 'microsoft/vscode' repo, esp those from base/common, like async.ts, strings.ts, map.ts etc pp. Instead of copying them manually and maintaining them in here, we can use them from the vscode repo. To do so, there is a the `script/setup/copySources.ts` script. Towards the end you'll find a list of modules that are copied from the vscode repo. If you need a module from vscode, add it to the list and run `npx tsx script/setup/copySources.ts`. Have this repo as sibling to the vscode repo and it will copy the modules from the vscode repo into `src/util/vs`. Note that the `src/util/vs` folder is marked as readonly and that changes to the copied sources should be carried out in the vscode repo. ## Developing Prompts We have developed a TSX-based framework for composing prompts. This section describes the problems it solves and how to use it. ### Motivations for TSX prompt crafting * Enable dynamic composition of OpenAI API request messages with respect to the token budget. * Prompts are bare strings, which makes them hard to edit once they are composed via string concatenation. Instead, with TSX prompting, messages are represented as a tree of TSX components. Each node in the tree has a `priority` that is conceptually similar to a `zIndex` (higher number == higher priority). If an intent declares more messages than can fit into the token budget, the prompt renderer prunes messages with the lowest priority from the `ChatMessage` array that is eventually sent to the Copilot API, preserving the order in which they were declared. * This also makes it easier to eventually support more sophisticated prompt management techniques, e.g. experimenting on variants of a prompt, or that a prompt part makes itself smaller with a Copilot API request to recursively summarize its children. * Make prompt crafting transparent to the owner of each LLM-based feature/intent while still enabling reuse of common prompt elements like safety rules. * Your intent owns and fully controls the `System`, `User` and `Assistant` messages that are sent to the Copilot API. This allows greater control and visibility into the safety rules, prompt context kinds, and conversation history that are sent for each feature. ### Quickstart - First define a root TSX prompt component extending [`PromptElement`]. The simplest prompt element implements a synchronous `render` method which returns the chat messages it wants to send to the Copilot API. For example: ```ts interface CatPromptProps extends BasePromptElementProps { query: string; } export class CatPrompt extends PromptElement { render() { return ( <> Respond to all messages as if you were a cat. {this.props.query} ); } } ``` - To render your prompt element, create an instance of [`PromptRenderer`] and call `render` on the prompt component you defined, passing in the props that your prompt component expects. `PromptRenderer` produces an array of system, user, and assistant messages which are suitable for sending to the Copilot API via the `ChatMLFetcher`. See this [OpenAI guide](https://platform.openai.com/docs/guides/prompt-engineering/six-strategies-for-getting-better-results) for some strategies to get good results. ```ts class CatIntentInvocation implements IIntentInvocation { constructor(private readonly accessor: ServicesAccessor, private readonly endpoint: IChatEndpoint, ) {} async buildPrompt({ query }: IBuildPromptContext, progress: vscode.Progress, token: vscode.CancellationToken): Promise { // Render the `CatPrompt` prompt element const renderer = new PromptRenderer(this.accessor, this.endpoint, CatPrompt, { query }); return renderer.render(progress, token); } } ``` - Prompt elements can return other prompt elements which will all be rendered by the prompt renderer. For example, your prompt may benefit from reusing the following utility components: - `SystemMessage`, `UserMessage` and `AssistantMessage`: Text within these components will be converted to the system, user and assistant message types from the OpenAI API. - `SafetyRules`: This should usually be included in a `SystemMessage` to ensure that your feature is compliant with Responsible AI guidelines. - If your prompt does asynchronous work e.g. VS Code extension API calls or additional requests to the Copilot API for chunk reranking, you can precompute this state in an optional async `prepare` method. `prepare` is called before `render` and the prepared state will be passed back to your prompt component's sync `render` method. Please note: * Newlines are not preserved in string literals when rendered, and must be explicitly declared with the builtin `
` attribute. * For now, if two prompt messages _with the same priority_ are up for eviction due to exceeding the token budget, it is not possible for a subtree of the prompt message declared before to evict a subtree of the prompt message declared later. ## Code structure ### Project Architecture and Coding Standards For comprehensive information about the project architecture, coding standards, and development guidelines, please refer to the [Copilot Instructions](.github/copilot-instructions.md). This document includes: * **Project Overview**: Key features, tech stack, and capabilities * **Architecture Details**: Directory structure, service organization, and extension activation flow * **Coding Standards**: TypeScript/JavaScript guidelines, React/JSX conventions, and architecture patterns * **Key Entry Points**: Where to make changes for specific features * **Development Guidelines**: Best practices for contributing to the codebase Understanding these guidelines is crucial for making effective contributions to the GitHub Copilot Chat extension. ### Layers Like in VS Code we organize our source code into layers and folders. Understand a "layer" as runtime target which is defined by the ambient APIs that you can use. We have these layers: * `common` - Just JavaScript and its builtins APIs. Also allowed to use types from the VS Code API, but no runtime access. * `vscode` - Runtime access to VS Code APIs, can use `common` * `node` - Node.js APIs and modules, can use `common, node` * `vscode-node` - VS Code APIs and Node.js APIs, can use `common, vscode, node` * `worker` - Web Worker APIs, can use `common` * `vscode-worker` - VS Code APIs and Web Worker APIs, can use `common, vscode, worker` Top-level folders are how we organize our code into logic groups, each folder has sub-folders, source files are inside a layer-folder. We have the following top-level folders - src - util - Utility code that can be used across the board - Files in this folder can be loaded by tests that run outside of vscode - They should import basic types from the `vscodeTypes` module, this will be shimmed for tests - Can't import from the `./platform` nor `./extension` folder - platform - This folder contains services that are used to implement extensions, like telemetry, configuration, search etc - Can import from `./util` - extension - This is the big folder where all functionality is implemented. - Can import from `./util` and `./platform` - test - Test code in this folder can import from `base/` but not `extension/` ### Runtimes (node.js, web worker) Copilot supports both node.js and web worker extension hosts, i.e. can run on desktop but also in web, even if no remote is connected ("serverless"). As such, we are building 2 flavors of the extension: * `./extension/extension/vscode-node/extension.ts`: extension runs in node.js extension hosts * `./extension/extension/vscode-worker/extension.ts`: extension runs in web worker extension hosts As much as possible, we try to run the same code in both node.js and web worker extension hosts. Having runtime specific code should be the exception and not the rule. Here are some examples of code that will not be supported in web worker extension hosts: * direct use of node.js API (for example `require`, `process.env`, `fs`) * use of node.js modules that are not built for the web * dependencies to other extensions that are unsupported in the web (for example `vscode.Git` extension) Running the extension out of sources in their runtimes: * `node`: just use the launch configuration ("Launch Copilot Extension") * `web` * ensure an entry `"browser": "./dist/web"` in `package.json` * run `npm run web` * in your browser open `http://localhost:3000` * in VS Code configure the hidden setting `chat.experimental.serverlessWebEnabled` to `true` (reload if this is the first time you set it) ### Contributions and Services Like in VS Code, Copilot extension is built with contributions and services so that components can both isolate from each other but also provide and use services together. Contributions are registered in these folders and automatically picked up by the extension when running: * `./extension/extension/vscode/contributions.ts`: contributions that can run in both node.js and web worker extension hosts * `./extension/extension/vscode-node/contributions.ts`: contributions that only run in node.js extension hosts * `./extension/extension/vscode-worker/contributions.ts`: contributions that only run in web worker extension hosts Similarly, services are registered and automatically picked up by the main instantiation service that creates these contributions: * `./extension/extension/vscode/services.ts`: services that can run in both node.js and web worker extension hosts * `./extension/extension/vscode-node/services.ts`: services that only run in node.js extension hosts * `./extension/extension/vscode-worker/services.ts`: services that only run in web worker extension hosts Again, try to make your services and contributions available in the `vscode` layer so that it can be used in all supported runtimes. ## Agent mode The main interesting files related to agent mode are: - [`agentPrompt.tsx`](src/extension/prompts/node/agent/agentPrompt.tsx): The main entrypoint for rendering the agent prompt - [`agentInstructions.tsx`](src/extension/prompts/node/agent/agentInstructions.tsx): The agent mode system prompt - [`toolCallingLoop.ts`](src/extension/intents/node/toolCallingLoop.ts): Running the agentic loop - [`chatAgents.ts`](src/extension/conversation/vscode-node/chatParticipants.ts): Registers agent mode and other participants, and the handlers for requests coming from VS Code. Currently, agent mode is essentially a [chat participant](https://code.visualstudio.com/api/extension-guides/chat) registered with VS Code. It mainly uses the standard API along with the standard [`vscode.lm.invokeTool`](https://code.visualstudio.com/api/references/vscode-api#lm.tools) API to invoke tools, but is registered with a flag in `package.json` denoting it as the "agent mode" participant. It also has some special abilities driven by [proposed API](https://code.visualstudio.com/api/advanced-topics/using-proposed-api). > **Note**: Some usages of "agent" in the codebase may refer to our older chat participants (`@workspace`, `@vscode`, ...) or Copilot Extension agents installed by a GitHub App. ## Tools Copilot registers a number of different tools. Tools are also available from other VS Code extensions or from MCP servers registered with VS Code. The tool picker in VS Code primarily determines which tools are enabled, and this set is passed to the agent on the ChatRequest. Some edit tools are only enabled for certain models or based on configuration or experiments. The agent has the final say for which tools are included in a request, and this logic is in `getTools` in [`agentIntent.ts`](src/extension/intents/node/agentIntent.ts). ### Developing tools Tools are registered through VS Code's normal [Language Model Tool API](https://code.visualstudio.com/api/extension-guides/tools). The key parts of the built-in tools are here: - [`package.json`](package.json): The tool descriptions and schemas are defined here. - [`toolNames.ts`](src/extension/tools/common/toolNames.ts): Contains the model-facing tool names. - [`tools/`](src/extension/tools/node/): Tool implementations are in this folder. For the most part, they are implementations of the standard `vscode.LanguageModelTool` interface, but since some have additional custom behavior, they can implement the extended `ICopilotTool` interface. See the [tools.md](docs/tools.md) document for more important details on how to develop tools. Please read it before adding a new tool! ## Tree Sitter We have now moved to https://github.com/microsoft/vscode-tree-sitter-wasm for WASM prebuilds. ## Troubleshooting ### Reading requests To easily see the details of requests made by Copilot Chat, run the command "Show Chat Debug View". This will show a treeview with an entry for each request made. You can see the prompt that was sent to the model, the tools that were enabled, the response, and other key details. Always read the prompt when making any changes, to ensure that it's being rendered as you expect! You can save the request log with right click > "Export As...". The view also has entries for tool calls on their own, and a prompt-tsx debug view that opens in the Simple Browser. > 🚨 **Note**: This log is also very helpful in troubleshooting issues, and we will appreciate if you share it when filing an issue about the agent's behavior. But, this log may contain personal information such as the contents of your files or terminal output. Please review the contents carefully before sharing it with anyone else. ## API updates When updating VS Code proposed extension API that is used by the extension, we have two tools to make sure that the version of the extension that gets installed will be compatible with the version of VS Code: the `engines.vscode` field in `package.json`, and the proposed API version. ### Making breaking changes to API When making a change to the proposed API that breaks backwards compatibility, you MUST update the API version of the proposal. This is declared in a comment at the top of the proposal .d.ts, and gets automatically updated in `extensionsApiProposals.ts` by the build task. Example: https://github.com/microsoft/vscode/blob/93a7382ecd63439a5bc507ef60e57610845ec05d/src/vscode-dts/vscode.proposed.lmTools.d.ts#L6. Then, you must adopt this change in the extension and declare that the extension supports this version of the API in `package.json`'s `enabledApiProposals`, like `lmTools@2`. This will ensure that the extension will only be installed and activated in a version of VS Code that supports the same version of the API. And, you must adopt this change in the extension at the same time as it's made in VS Code, otherwise the next day's Insiders build won't have a compatible Copilot Chat extension available. Examples of changes that break backwards compatibility: - Renaming a method that is used by the extension - Changing the parameters that an existing method takes - Adding a required static contribution point for a proposal that the extension already uses ### Making additive changes to API When making a change to proposed API that adds a new feature but doesn't break backwards compatibility, you don't have to update the API version, because an older version of the extension will still work with the new VS Code build. But, once you adopt that new API, you must update the date part of the `engines.vscode` field in `package.json`. For example, `"vscode": "^1.91.0-20240624"`. This ensures that the extension will only be installed and activated in a version of VS Code that supports the new API. Examples of additive changes - Adding a new response type to `ChatResponseStream` - Adding a new API proposal - Adding a new method to an existing interface ## Running with Code OSS ### Desktop You can run the extension from Code OSS Desktop, provided that you follow along these steps: - Create a top level `product.overrides.json` in the `vscode` repository - Add below contents as JSON - Run the extension launch configuration in Code OSS ```json { "trustedExtensionAuthAccess": { "github": [ "github.copilot-chat" ] } } ``` ### Web Code OSS for Web unfortunately does not support the `product.overrides.json` trick. You have to manually copy the contents of the `defaultChatAgent` property into the `src/vs/platform/product/common/product.ts` file [here](https://github.com/microsoft/vscode/blob/d499211732305086bbac4e603392e540dee05bd2/src/vs/platform/product/common/product.ts#L72). For example: ```ts Object.assign(product, { version: '1.102.0-dev', nameShort: 'Code - OSS Dev', nameLong: 'Code - OSS Dev', applicationName: 'code-oss', dataFolderName: '.vscode-oss', urlProtocol: 'code-oss', reportIssueUrl: 'https://github.com/microsoft/vscode/issues/new', licenseName: 'MIT', licenseUrl: 'https://github.com/microsoft/vscode/blob/main/LICENSE.txt', serverLicenseUrl: 'https://github.com/microsoft/vscode/blob/main/LICENSE.txt', defaultChatAgent: { 'extensionId': 'GitHub.copilot', 'chatExtensionId': 'GitHub.copilot-chat', 'documentationUrl': 'https://aka.ms/github-copilot-overview', 'termsStatementUrl': 'https://aka.ms/github-copilot-terms-statement', 'privacyStatementUrl': 'https://aka.ms/github-copilot-privacy-statement', 'skusDocumentationUrl': 'https://aka.ms/github-copilot-plans', 'publicCodeMatchesUrl': 'https://aka.ms/github-copilot-match-public-code', 'manageSettingsUrl': 'https://aka.ms/github-copilot-settings', 'managePlanUrl': 'https://aka.ms/github-copilot-manage-plan', 'manageOverageUrl': 'https://aka.ms/github-copilot-manage-overage', 'upgradePlanUrl': 'https://aka.ms/github-copilot-upgrade-plan', 'signUpUrl': 'https://aka.ms/github-sign-up', 'provider': { 'default': { 'id': 'github', 'name': 'GitHub' }, 'enterprise': { 'id': 'github-enterprise', 'name': 'GHE.com' }, 'google': { 'id': 'google', 'name': 'Google' }, 'apple': { 'id': 'apple', 'name': 'Apple' } }, 'providerUriSetting': 'github-enterprise.uri', 'providerScopes': [ [ 'user:email' ], [ 'read:user' ], [ 'read:user', 'user:email', 'repo', 'workflow' ] ], 'entitlementUrl': 'https://api.github.com/copilot_internal/user', 'entitlementSignupLimitedUrl': 'https://api.github.com/copilot_internal/subscribe_limited_user', 'chatQuotaExceededContext': 'github.copilot.chat.quotaExceeded', 'completionsQuotaExceededContext': 'github.copilot.completions.quotaExceeded', 'walkthroughCommand': 'github.copilot.open.walkthrough', 'completionsMenuCommand': 'github.copilot.toggleStatusMenu', 'completionsRefreshTokenCommand': 'github.copilot.signIn', 'chatRefreshTokenCommand': 'github.copilot.refreshToken', 'completionsAdvancedSetting': 'github.copilot.advanced', 'completionsEnablementSetting': 'github.copilot.enable', 'nextEditSuggestionsSetting': 'github.copilot.nextEditSuggestions.enabled' }, trustedExtensionAuthAccess: { 'github': [ 'github.copilot-chat' ] } }); } ``` ================================================ FILE: CodeQL.yml ================================================ path_classifiers: tests: - "test/simulation/fixtures/edit-asyncawait-4151/*.ts" - "test/simulation/fixtures/edit-slice-4149/*.ts" - src/platform/parser/test/node/fixtures ================================================ FILE: LICENSE.txt ================================================ MIT License Copyright (c) Microsoft Corporation. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # GitHub Copilot - Your autonomous AI peer programmer **[GitHub Copilot](https://code.visualstudio.com/docs/copilot/overview)** is an AI peer programming tool that transforms how you write code in Visual Studio Code. GitHub Copilot agents handle complete coding tasks end-to-end, autonomously planning work, editing files, running commands, and self-correcting when they hit errors. You can also leverage inline suggestions for quick coding assistance and inline chat for precise, focused edits directly in the editor. **Sign up for [GitHub Copilot Free](https://github.com/settings/copilot?utm_source=vscode-chat-readme&utm_medium=first&utm_campaign=2025mar-em-MSFT-signup)!** ![Working with GitHub Copilot agent mode to make edits to code in your workspace](https://github.com/microsoft/vscode-docs/raw/732b9599e49ee7034744a3e5b0485b7fb4bdf530/docs/copilot/images/getting-started/custom-reviewer-mode.png) ## Getting access to GitHub Copilot Sign up for [GitHub Copilot Free](https://github.com/settings/copilot?utm_source=vscode-chat-readme&utm_medium=second&utm_campaign=2025mar-em-MSFT-signup), or request access from your enterprise admin. To access GitHub Copilot, an active GitHub Copilot subscription is required. You can read more about our business and individual offerings at [github.com/features/copilot](https://github.com/features/copilot?utm_source=vscode-chat&utm_medium=readme&utm_campaign=2025mar-em-MSFT-signup). ## Build with autonomous agents **Let AI agents implement complex features end-to-end**. Give an agent a high-level task and it breaks the work into steps, edits multiple files, runs terminal commands, and self-corrects when it hits errors or failing tests. Agents excel at [building new features](https://code.visualstudio.com/docs/copilot/agents/overview), [debugging and fixing failing tests](https://code.visualstudio.com/docs/copilot/guides/debug-with-copilot), refactoring codebases, and [collaborating via pull requests](https://code.visualstudio.com/docs/copilot/agents/cloud-agents). **Manage sessions from a central view.** Run multiple [agent sessions](https://code.visualstudio.com/docs/copilot/chat/chat-sessions) in parallel and track them in one place. Monitor session status, switch between active work, review file changes, and resume where you left off. **Run agents with your preferred harness.** Use agents locally in VS Code, in the background via Copilot CLI, or Cloud via Copilot Coding Agent. You can also work with providers like Claude and Codex, and hand tasks off between agent types with context preserved all within the VS Code. ![Video showing an agent session building a complete feature in VS Code.](https://github.com/microsoft/vscode-docs/raw/refs/heads/main/docs/copilot/images/overview/agents-intro.gif) **Use agents to [plan before you build](https://code.visualstudio.com/docs/copilot/agents/planning) with the Plan agent**, which breaks tasks into structured implementation plans and asks clarifying questions. When your plan is ready, hand it off to an implementation agent to execute it. You can also [delegate tasks to cloud agents](https://code.visualstudio.com/docs/copilot/agents/cloud-agents) that create branches, implement changes, and open pull requests for your team to review. ## More ways to code with AI **Receive intelligent inline suggestions** as you type with [ghost text suggestions](https://aka.ms/vscode-completions) and [next edit suggestions](https://aka.ms/vscode-nes), helping you write code faster. Copilot predicts your next logical change, and you can accept suggestions with the Tab key. ![Video showing Copilot next edit suggestions.](https://github.com/microsoft/vscode-docs/raw/refs/heads/main/docs/copilot/images/inline-suggestions/nes-video.gif) **Use inline chat for targeted edits** by pressing `Ctrl+I`/`Cmd+I` to open a chat prompt directly in the editor. Describe a change and Copilot suggests edits in place for refactoring methods, adding error handling, or explaining complex algorithms without leaving your editor. ![Inline chat in VS Code](https://code.visualstudio.com/assets/docs/copilot/copilot-chat/inline-chat-question-example.png) ## Customize AI for your workflow **Agents work best when they understand your project's conventions and have the right tools**. Tailor Copilot so it generates code that fits your codebase from the start. **Project context.** Use [custom instructions](https://code.visualstudio.com/docs/copilot/customization/custom-instructions) to specify project-wide or task-specific context and coding guidelines. **Add specialized capabilities**. Teach Copilot specialized capabilities with [agent skills](https://code.visualstudio.com/docs/copilot/customization/agent-skills) or define specialized personas with [custom agents](https://code.visualstudio.com/docs/copilot/customization/custom-agents). **Connect to external tools and services**. Extend agents further with tools from [MCP servers](https://code.visualstudio.com/docs/copilot/customization/mcp-servers) and extensions to give Copilot a gateway to external data sources, APIs, or specialized tools. ### Supported languages and frameworks GitHub Copilot works on any language, including Java, PHP, Python, JavaScript, Ruby, Go, C#, or C++. Because it’s been trained on languages in public repositories, it works for most popular languages, libraries and frameworks. ### Version compatibility As Copilot Chat releases in lockstep with VS Code due to its deep UI integration, every new version of Copilot Chat is only compatible with the latest and newest release of VS Code. This means that if you are using an older version of VS Code, you will not be able to use the latest Copilot Chat. Only the latest Copilot Chat versions will use the latest models provided by the Copilot service, as even minor model upgrades require prompt changes and fixes in the extension. ### Privacy and preview terms By using Copilot Chat you agree to [GitHub Copilot chat preview terms](https://docs.github.com/en/early-access/copilot/github-copilot-chat-technical-preview-license-terms). Review the [transparency note](https://aka.ms/CopilotChatTransparencyNote) to understand about usage, limitations and ways to improve Copilot Chat during the technical preview. Your code is yours. We follow responsible practices in accordance with our [Privacy Statement](https://docs.github.com/en/site-policy/privacy-policies/github-privacy-statement) to ensure that your code snippets will not be used as suggested code for other users of GitHub Copilot. To get the latest security fixes, please use the latest version of the Copilot extension and VS Code. ### Resources & next steps * **[Sign up for GitHub Copilot Free](https://github.com/settings/copilot?utm_source=vscode-chat-readme&utm_medium=third&utm_campaign=2025mar-em-MSFT-signup)**: Explore Copilot's AI capabilities at no cost before upgrading to a paid plan. * If you're using Copilot for your business, check out [Copilot Business](https://docs.github.com/en/copilot/copilot-business/about-github-copilot-business) and [Copilot Enterprise](https://docs.github.com/en/copilot/github-copilot-enterprise/overview/about-github-copilot-enterprise). * **[Copilot Quickstart](https://code.visualstudio.com/docs/copilot/getting-started)**: Discover the key features of Copilot in VS Code. * **[Agents Tutorial](https://code.visualstudio.com/docs/copilot/agents/agents-tutorial)**: Get started with autonomous agents across different environments. * **[VS Code on YouTube](https://www.youtube.com/@code)**: Watch the latest demos and updates on the VS Code channel. * **[Frequently Asked Questions](https://code.visualstudio.com/docs/copilot/faq)**: Get answers to commonly asked questions about Copilot in VS Code. * **[Provide Feedback](https://github.com/microsoft/vscode-copilot-release/issues)**: Send us your feedback and feature request to help us make GitHub Copilot better! ## Data and telemetry The GitHub Copilot Extension for Visual Studio Code collects usage data and sends it to Microsoft to help improve our products and services. Read our [privacy statement](https://privacy.microsoft.com/privacystatement) to learn more. This extension respects the `telemetry.telemetryLevel` setting which you can learn more about at https://code.visualstudio.com/docs/supporting/faq#_how-to-disable-telemetry-reporting. ## Trademarks This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow Microsoft's Trademark & Brand Guidelines. Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. ## License Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the [MIT](LICENSE.txt) license. ================================================ FILE: SECURITY.md ================================================ ## Security Microsoft takes the security of our software products and services seriously, which includes all source code repositories in our GitHub organizations. **Please do not report security vulnerabilities through public GitHub issues.** For security reporting information, locations, contact information, and policies, please review the latest guidance for Microsoft repositories at [https://aka.ms/SECURITY.md](https://aka.ms/SECURITY.md). ================================================ FILE: assets/prompts/create-agent.prompt.md ================================================ --- name: create-agent description: 'Create a custom agent (.agent.md) for a specific job.' argument-hint: What job should this agent do and how? agent: agent --- Related skill: `agent-customization`. Load and follow **agents.md** for template and principles. Guide the user to create an `.agent.md`. ## Extract from Conversation First, review the conversation history. If the user has been using the agent in a specialized way (e.g., restricting tools, following a specific persona, focusing on certain file types), generalize that into a custom agent. Extract: - The specialized role or persona being assumed - Tool preferences (which to use, which to avoid) - The domain or job scope ## Clarify if Needed If no clear specialization emerges from the conversation, clarify: - What job should this agent do? - When should it be picked over the default agent? - Which tools should it use (or avoid)? ## Iterate 1. Draft the agent file and save it. 2. Identify the most ambiguous or weak parts and ask about those. 3. Once finalized, summarize what the agent does, suggest example prompts to try it, and propose related customizations to create next. Remember to follow the `agent-customization` guidelines to create highly effective agents. ================================================ FILE: assets/prompts/create-hook.prompt.md ================================================ --- name: create-hook description: 'Create a hook (.json) to enforce policy or automate agent lifecycle events.' argument-hint: What should be enforced or automated? agent: agent --- Related skill: `agent-customization`. Load and follow **hooks.md** for template and principles. Guide the user to create a hook in `.github/hooks/`. ## Extract from Conversation First, review the conversation history. If the user has been expressing concerns about agent behavior (e.g., "don't run this command", "always check before doing X", "inject this context"), generalize that into a hook. Extract: - Actions that should be blocked or gated - Context that should be injected at certain points - Automation needs at session start/end or tool use ## Clarify if Needed If no clear policy need emerges from the conversation, clarify: - What event should trigger this hook? (e.g. PreToolUse, SessionStart, Stop) - Should it block, warn, or inject context? - Does it need a companion script? ## Iterate 1. Draft the hook JSON (and any scripts) and save them. 2. Identify the most ambiguous or weak parts and ask about those. 3. Once finalized, summarize what the hook enforces, suggest ways to test it, and propose related customizations to create next. Remember to follow the `agent-customization` guidelines to create highly effective hooks. ================================================ FILE: assets/prompts/create-instructions.prompt.md ================================================ --- name: create-instructions description: 'Create an instructions file (.instructions.md) for a project rule or convention.' argument-hint: What rule or convention to enforce? agent: agent --- Related skill: `agent-customization`. Load and follow **instructions.md** for template and principles. Guide the user to create an instructions file. ## Extract from Conversation First, review the conversation history. If the user has been correcting the agent's output or asking for specific patterns (e.g., "always use X", "never do Y", "follow this style"), generalize that into a persistent instruction. Extract: - Corrections or preferences mentioned during the conversation - Coding patterns the user enforced or requested - Project-specific conventions referenced ## Clarify if Needed If no clear rule emerges from the conversation, clarify: - Should this apply everywhere or only to specific files? - Which technologies or file types are affected? - Is this a hard rule or a preference? Explore the codebase using subagents if you need more context. ## Iterate 1. Draft the instruction and save it. 2. Identify the most ambiguous or weak parts and ask about those. 3. Once finalized, summarize what the instruction enforces, suggest example prompts to see it in action, and propose related customizations to create next. Remember to follow the `agent-customization` guidelines to create highly effective instructions. ================================================ FILE: assets/prompts/create-prompt.prompt.md ================================================ --- name: create-prompt description: 'Create a reusable prompt file (.prompt.md) for a common task.' argument-hint: What task should this prompt help with? agent: agent --- Related skill: `agent-customization`. Load and follow **prompts.md** for template and principles. Guide the user to create a `.prompt.md`. ## Extract from Conversation First, review the conversation history. If the user has been working on a repeatable task pattern (e.g., explaining code, generating tests, refactoring), generalize that into a reusable prompt. Extract: - The core task being performed repeatedly - Any implicit inputs (selected code, file type, context) - The desired output format or style ## Clarify if Needed If no clear pattern emerges from the conversation, clarify: - What task should this prompt help with? - Should it take arguments or use fixed context? - Workspace-scoped or personal? ## Iterate 1. Draft the prompt and save it. 2. Identify the most ambiguous or weak parts and ask about those. 3. Once finalized, summarize what the prompt does, suggest example invocations, and propose related customizations to create next. Remember to follow the `agent-customization` guidelines to create highly effective prompts. ================================================ FILE: assets/prompts/create-skill.prompt.md ================================================ --- name: create-skill description: 'Create a reusable skill (SKILL.md) that packages a workflow.' argument-hint: What should this skill produce? agent: agent --- Related skill: `agent-customization`. Load and follow **skills.md** for template and principles. Guide the user to create a `SKILL.md`. ## Extract from Conversation First, review the conversation history. If the user has been following a multi-step workflow or methodology (e.g., debugging approach, review checklist, implementation pattern), generalize that into a reusable skill. Extract: - The step-by-step process being followed - Decision points and branching logic - Quality criteria or completion checks ## Clarify if Needed If no clear workflow emerges from the conversation, clarify: - What outcome should this skill produce? - Workspace-scoped or personal? - Quick checklist or full multi-step workflow? ## Iterate 1. Draft the skill and save it. 2. Identify the most ambiguous or weak parts and ask about those. 3. Once finalized, summarize what the skill produces, suggest example prompts to try it, and propose related customizations to create next. Remember to follow the `agent-customization` guidelines to create highly effective skills. ================================================ FILE: assets/prompts/init.prompt.md ================================================ --- name: init description: Generate or update workspace instructions file for AI coding agents argument-hint: Optionally specify a focus area or pattern to document for agents agent: agent --- Related skill: `agent-customization`. Load and follow **workspace-instructions.md** for template, principles, and anti-patterns. Bootstrap workspace instructions (`.github/copilot-instructions.md` or `AGENTS.md` if already present). ## Workflow 1. **Discover existing conventions** Search: `**/{.github/copilot-instructions.md,AGENT.md,AGENTS.md,CLAUDE.md,.cursorrules,.windsurfrules,.clinerules,.cursor/rules/**,.windsurf/rules/**,.clinerules/**,README.md}` 2. **Explore the codebase** via subagent, 1-3 in parallel if needed Find essential knowledge that helps an AI agent be immediately productive: - Build/test commands (agents run these automatically) - Architecture decisions and component boundaries - Project-specific conventions that differ from common practices - Potential pitfalls or common development environment issues - Key files/directories that exemplify patterns Also inventory existing documentation (`docs/**/*.md`, `CONTRIBUTING.md`, `ARCHITECTURE.md`, etc.) to identify topics that should be linked, not duplicated. 3. **Generate or merge** - New file: Use template from workspace-instructions.md, include only relevant sections - Existing file: Preserve valuable content, update outdated sections, remove duplication - Follow the **Link, don't embed** principle from workspace-instructions.md 4. **Iterate** - Ask for feedback on unclear or incomplete sections - If the workspace is complex, suggest applyTo-based instructions for specific areas (e.g., frontend, backend, tests) Once finalized, suggest example prompts to see it in action, and propose related agent-customizations to create next (`/create-(agent|hook|instruction|prompt|skill) …`), explaining the customization and how it would be used in practice. ================================================ FILE: assets/prompts/plan.prompt.md ================================================ --- name: plan description: Research and plan with the Plan agent agent: Plan argument-hint: Describe what you want to plan or research --- Plan my task. ================================================ FILE: assets/prompts/skills/agent-customization/SKILL.md ================================================ --- name: agent-customization description: '**WORKFLOW SKILL** — Create, update, review, fix, or debug VS Code agent customization files (.instructions.md, .prompt.md, .agent.md, SKILL.md, copilot-instructions.md, AGENTS.md). USE FOR: saving coding preferences; troubleshooting why instructions/skills/agents are ignored or not invoked; configuring applyTo patterns; defining tool restrictions; creating custom agent modes or specialized workflows; packaging domain knowledge; fixing YAML frontmatter syntax. DO NOT USE FOR: general coding questions (use default agent); runtime debugging or error diagnosis; MCP server configuration (use MCP docs directly); VS Code extension development. INVOKES: file system tools (read/write customization files), ask-questions tool (interview user for requirements), subagents for codebase exploration. FOR SINGLE OPERATIONS: For quick YAML frontmatter fixes or creating a single file from a known pattern, edit the file directly — no skill needed.' --- # Agent Customization ## Decision Flow | Primitive | When to Use | |-----------|-------------| | Workspace Instructions | Always-on, applies everywhere in the project | | File Instructions | Explicit via `applyTo` patterns, or on-demand via `description` | | MCP | Integrates external systems, APIs, or data | | Hooks | Deterministic shell commands at agent lifecycle points (block tools, auto-format, inject context) | | Custom Agents | Subagents for context isolation, or multi-stage workflows with tool restrictions | | Prompts | Single focused task with parameterized inputs | | Skills | On-demand workflow with bundled assets (scripts/templates) | ## Quick Reference Consult the reference docs for templates, domain examples, advanced frontmatter options, asset organization, anti-patterns, and creation checklists. If the references are not enough, load the official documentation links for each primitive. | Type | File | Location | Reference | |------|------|----------|-----------| | Workspace Instructions | `copilot-instructions.md`, `AGENTS.md` | `.github/` or root | [Link](./references/workspace-instructions.md) | | File Instructions | `*.instructions.md` | `.github/instructions/` | [Link](./references/instructions.md) | | Prompts | `*.prompt.md` | `.github/prompts/` | [Link](./references/prompts.md) | | Hooks | `*.json` | `.github/hooks/` | [Link](./references/hooks.md) | | Custom Agents | `*.agent.md` | `.github/agents/` | [Link](./references/agents.md) | | Skills | `SKILL.md` | `.github/skills//`, `.agents/skills//`, `.claude/skills//` | [Link](./references/skills.md) | **User-level**: `{{USER_PROMPTS_FOLDER}}/` (*.prompt.md, *.instructions.md, *.agent.md; not skills) Customizations roam with user's settings sync ## Creation Process If you need to explore or validate patterns in the codebase, use a read-only subagent. If the ask-questions tool is available, use it to interview the user and clarify requirements. Follow these steps when creating any customization file. ### 1. Determine Scope Ask the user where they want the customization: - **Workspace**: For project-specific, team-shared customizations → `.github/` folder - **User profile**: For personal, cross-workspace customizations → `{{USER_PROMPTS_FOLDER}}/` ### 2. Choose the Right Primitive Use the Decision Flow above to select the appropriate file type based on the user's need. ### 3. Create the File Create the file directly at the appropriate path: - Use the location tables in each reference file - Include required frontmatter as needed - Add the body content following the templates ### 4. Validate After creating: - Confirm the file is in the correct location - Verify frontmatter syntax (YAML between `---` markers) - Check that `description` is present and meaningful ## Edge Cases **Instructions vs Skill?** Does this apply to *most* work, or *specific* tasks? Most → Instructions. Specific → Skill. **Skill vs Prompt?** Both appear as slash commands in chat (type `/`). Multi-step workflow with bundled assets → Skill. Single focused task with inputs → Prompt. **Skill vs Custom Agent?** Same capabilities for all steps → Skill. Need context isolation (subagent returns single output) or different tool restrictions per stage → Custom Agent. **Hooks vs Instructions?** Instructions *guide* agent behavior (non-deterministic). Hooks *enforce* behavior via shell commands at lifecycle events like `PreToolUse` or `PostToolUse` — they can block operations, require approval, or run formatters deterministically. See [hooks reference](./references/hooks.md). ## Common Pitfalls **Description is the discovery surface.** The `description` field is how the agent decides whether to load a skill, instruction, or agent. If trigger phrases aren't IN the description, the agent won't find it. Use the "Use when..." pattern with specific keywords. **YAML frontmatter silent failures.** Unescaped colons in values, tabs instead of spaces, `name` that doesn't match folder name — all cause silent failures with no error message. Always quote descriptions that contain colons: `description: "Use when: doing X"`. **`applyTo: "**"` burns context.** This means "always included for every file request" — it loads the instruction into the context window on every interaction, even when irrelevant. Use specific globs (`**/*.py`, `src/api/**`) unless the instruction truly applies to all files. ================================================ FILE: assets/prompts/skills/agent-customization/references/agents.md ================================================ # [Custom Agents (.agent.md)](https://code.visualstudio.com/docs/copilot/customization/custom-agents) Custom personas with specific tools, instructions, and behaviors. Use for orchestrated workflows with role-based tool restrictions. ## Locations | Path | Scope | |------|-------| | `.github/agents/*.agent.md` | Workspace | | `/agents/*.agent.md` | User profile | ## Frontmatter ```yaml --- description: "" # For agent picker and subagent discovery name: "Agent Name" # Optional, defaults to filename tools: [search, web] # Optional: aliases, MCP (/*), extension tools model: "Claude Sonnet 4" # Optional, uses picker default; supports array for fallback argument-hint: "Task..." # Optional, input guidance agents: [agent1, agent2] # Optional, restrict allowed subagents by name (omit = all, [] = none) user-invocable: true # Optional, show in agent picker (default: true) disable-model-invocation: false # Optional, prevent subagent invocation (default: false) handoffs: [...] # Optional, transitions to other agents --- ``` ### Invocation Control | Attribute | Default | Effect | |-----------|---------|--------| | `user-invocable: false` | `true` | Hide from agent picker, only accessible as subagent | | `disable-model-invocation: true` | `false` | Prevent other agents from invoking as subagent | ### Model Fallback ```yaml model: ['Claude Sonnet 4.5 (copilot)', 'GPT-5 (copilot)'] # First available model is used ``` ## Tools Sources: built-in aliases, specific tools, MCP servers (`/*`), extension tools. **Special**: `[]` = no tools, omit = defaults. Body reference: `#tool:` ### Tool Aliases | Alias | Purpose | |-------|---------| | `execute` | Run shell commands | | `read` | Read file contents | | `edit` | Edit files | | `search` | Search files or text | | `agent` | Invoke custom agents as subagents | | `web` | Fetch URLs and web search | | `todo` | Manage task lists | ### Common Patterns ```yaml tools: [read, search] # Read-only research tools: [myserver/*] # MCP server only tools: [read, edit, search] # No terminal access tools: [] # Conversational only ``` To discover available tools, check your current tool list or use `#tool:` syntax in the body to reference specific tools. ## Template ```markdown --- description: "{Use when... trigger phrases for subagent discovery}" tools: [{minimal set of tool aliases}] user-invocable: false --- You are a specialist at {specific task}. Your job is to {clear purpose}. ## Constraints - DO NOT {thing this agent should never do} - DO NOT {another restriction} - ONLY {the one thing this agent does} ## Approach 1. {Step one of how this agent works} 2. {Step two} 3. {Step three} ## Output Format {Exactly what this agent should return} ``` ## Invocation - **Manual**: Agent selector in chat - **Subagent**: Parent agent delegates based on `description` match (when `infer` allows) ## Core Principles 1. **Single role**: One persona with focused responsibilities per agent 2. **Minimal tools**: Only include what the role needs—excess tools dilute focus 3. **Clear boundaries**: Define what the agent should NOT do 4. **Keyword-rich description**: Include trigger words so parent agents know when to delegate ## Anti-patterns - **Swiss-army agents**: Too many tools, tries to do everything - **Vague descriptions**: "A helpful agent" doesn't guide delegation—be specific - **Role confusion**: Description doesn't match body persona - **Circular handoffs**: A → B → A without progress criteria ================================================ FILE: assets/prompts/skills/agent-customization/references/hooks.md ================================================ # [Hooks (.json)](https://code.visualstudio.com/docs/copilot/customization/hooks) Deterministic lifecycle automation for agent sessions. Use hooks to enforce policy, automate validation, and inject runtime context. ## Locations | Path | Scope | |------|-------| | `.github/hooks/*.json` | Workspace (team-shared) | | `.claude/settings.local.json` | Workspace local (not committed) | | `.claude/settings.json` | Workspace | | `~/.claude/settings.json` | User profile | Hooks from all configured locations are collected and executed; workspace and user hooks do not override each other. ## Hook Events | Event | Trigger | |------|-------| | `SessionStart` | First prompt of a new agent session | | `UserPromptSubmit` | User submits a prompt | | `PreToolUse` | Before tool invocation | | `PostToolUse` | After successful tool invocation | | `PreCompact` | Before context compaction | | `SubagentStart` | Subagent starts | | `SubagentStop` | Subagent ends | | `Stop` | Agent session ends | ## Configuration Format ```json { "hooks": { "PreToolUse": [ { "type": "command", "command": "./scripts/validate-tool.sh", "timeout": 15 } ] } } ``` Each hook command supports: - `type` (must be `command`) - `command` (default) - `windows`, `linux`, `osx` (platform overrides) - `cwd`, `env`, `timeout` ## Input / Output Contract Hooks receive JSON on stdin and can return JSON on stdout. - Common output: `continue`, `stopReason`, `systemMessage` - `PreToolUse` permissions are read from `hookSpecificOutput.permissionDecision` (`allow` | `ask` | `deny`) - `PostToolUse` output can block further processing with `decision: block` `PreToolUse` example output: ```json { "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "ask", "permissionDecisionReason": "Needs user confirmation" } } ``` Exit codes: - `0` success - `2` blocking error - Other values produce non-blocking warnings ## Hooks vs Other Customizations | Primitive | Behavior | |------|-------| | Instructions / Prompts / Skills / Agents | Guidance (non-deterministic) | | Hooks | Runtime enforcement and deterministic automation | Use hooks when behavior must be guaranteed (for example: block dangerous commands, force validation, auto-inject context). ## Core Principles 1. Keep hooks small and auditable 2. Validate and sanitize hook inputs 3. Avoid hardcoded secrets in scripts 4. Prefer workspace hooks for team policy, user hooks for personal automation ## Anti-patterns - Running long hooks that block normal flow - Using hooks where plain instructions are sufficient - Letting agents edit hook scripts without approval controls ================================================ FILE: assets/prompts/skills/agent-customization/references/instructions.md ================================================ # [File-Specific Instructions (.instructions.md)](https://code.visualstudio.com/docs/copilot/customization/custom-instructions) Guidelines loaded on-demand when relevant to the current task, or explicitly when files match a pattern. ## Locations | Path | Scope | |------|-------| | `.github/instructions/*.instructions.md` | Workspace | | `/instructions/*.instructions.md` | User profile | ## Frontmatter ```yaml --- description: "" # For on-demand discovery—keyword-rich name: "Instruction Name" # Optional, defaults to filename applyTo: "**/*.ts" # Optional, auto-attach for matching files --- ``` ## Discovery Modes | Mode | Trigger | Use Case | |------|---------|----------| | **On-demand** (`description`) | Agent detects task relevance | Task-based: migrations, refactoring, API work | | **Explicit** (`applyTo`) | Files matching glob in context | File-based: language standards, framework rules | | **Manual** | `Add Context` → `Instructions` | Ad-hoc attachment | ## Template ```markdown --- description: "Use when writing database migrations, schema changes, or data transformations. Covers safety checks and rollback patterns." --- # Migration Guidelines - Always create reversible migrations - Test rollback before merging - Never drop columns in the same release as code removal ``` Note the "Use when..." pattern in the description—this helps on-demand discovery. ## Explicit File Matching (optional) Use `applyTo` when the instruction applies to specific file types or folders: ```yaml applyTo: "**" # ALWAYS included, no matter the file or description (use with caution) applyTo: "**/*.py" # All Python files applyTo: ["src/**", "lib/**"] # Multiple patterns (OR) applyTo: src/**, lib/** # Multiple patterns without array syntax (OR) applyTo: "src/api/**/*.ts" # Specific folder + extension ``` Applied when creating or modifying matching files, not for read-only operations. ## Core Principles 1. **Keyword-rich descriptions**: Include trigger words for on-demand discovery 2. **One concern per file**: Separate files for testing, styling, documentation 3. **Concise and actionable**: Share context window—keep focused 4. **Show, don't tell**: Brief code examples over lengthy explanations ## Anti-patterns - **Vague descriptions**: "Helpful coding tips" doesn't enable discovery - **Overly broad applyTo**: `"**"` with content only relevant to specific files - **Duplicating docs**: Copy README instead of linking - **Mixing concerns**: Testing + API design + styling in one file ================================================ FILE: assets/prompts/skills/agent-customization/references/prompts.md ================================================ # [Prompts (.prompt.md)](https://code.visualstudio.com/docs/copilot/customization/prompt-files) Reusable task templates triggered on-demand in chat. Single focused task with parameterized inputs. ## Locations | Path | Scope | |------|-------| | `.github/prompts/*.prompt.md` | Workspace | | `/prompts/*.prompt.md` | User profile | ## Frontmatter ```yaml --- description: "" # Optional, but improves discoverability name: "Prompt Name" # Optional, defaults to filename argument-hint: "Task..." # Optional: hint shown in chat input agent: "agent" # Optional: ask, agent, plan, or custom agent model: "GPT-5 (copilot)" # Optional: selected model, or fallback array tools: [search, web] # Optional: built-in, tool sets, MCP (/*), extension --- ``` Model fallback is supported: ```yaml model: ['GPT-5 (copilot)', 'Claude Sonnet 4.5 (copilot)'] ``` ## Template ```markdown --- description: "Generate test cases for selected code" agent: "agent" --- Generate comprehensive test cases for the provided code: - Include edge cases and error scenarios - Follow existing test patterns in the codebase - Use descriptive test names ``` **Context references**: Use Markdown links for files (`[config](./config.json)`) and `#tool:` for tools. ## Invocation - **Chat**: Type `/` → select from prompts and skills - **Command**: `Chat: Run Prompt...` - **Editor**: Open prompt file → play button > Both prompts and skills appear as slash commands in chat. Skills provide multi-step workflows with bundled assets; prompts are single focused tasks. **Tip**: Use `chat.promptFilesRecommendations` to show prompts as actions when starting a new chat. ## Tool Priority When both prompt and custom agent define tools: 1. Tools from prompt file 2. Tools from referenced custom agent 3. Default tools for selected agent ## When to Use - Generate test cases for specific code - Create READMEs from specs - Summarize metrics with custom parameters - One-off generation tasks ## Core Principles 1. **Single task focus**: One prompt = one well-defined task 2. **Output examples**: Show expected format when quality depends on structure 3. **Reuse over duplication**: Reference instruction files instead of copying ## Anti-patterns - **Multi-task prompts**: "create and test and deploy" in one prompt - **Vague descriptions**: Descriptions that don't help users understand when to use - **Over-tooling**: Many tools when the task only needs search or file access ================================================ FILE: assets/prompts/skills/agent-customization/references/skills.md ================================================ # [Agent Skills (SKILL.md)](https://code.visualstudio.com/docs/copilot/customization/agent-skills) Folders of instructions, scripts, and resources that agents load on-demand for specialized tasks. ## Structure ``` .github/skills// ├── SKILL.md # Required (name must match folder) ├── scripts/ # Executable code ├── references/ # Docs loaded as needed └── assets/ # Templates, boilerplate ``` ## Locations | Path | Scope | |------|-------| | `.github/skills//` | Project | | `.agents/skills//` | Project | | `.claude/skills//` | Project | | `~/.copilot/skills//` | Personal | | `~/.agents/skills//` | Personal | | `~/.claude/skills//` | Personal | ## SKILL.md Format ```yaml --- name: skill-name # Required: 1-64 chars, lowercase alphanumeric + hyphens, must match folder description: 'What and when to use. Max 1024 chars.' argument-hint: 'Optional hint shown for slash invocation' user-invocable: true # Optional: show as slash command (default: true) disable-model-invocation: false # Optional: disable automatic model-triggered loading --- ``` ### Body - What the skill accomplishes - When to use (triggers and use cases) - Step-by-step procedures - References to resources: `[script](./scripts/test.js)` ## Template ```markdown --- name: webapp-testing description: 'Test web applications using Playwright. Use for verifying frontend, debugging UI, capturing screenshots.' --- # Web Application Testing ## When to Use - Verify frontend functionality - Debug UI behavior ## Procedure 1. Start the web server 2. Run [test script](./scripts/test.js) 3. Review screenshots in `./screenshots/` ``` ## Progressive Loading 1. **Discovery** (~100 tokens): Agent reads `name` and `description` 2. **Instructions** (<5000 tokens): Loads `SKILL.md` body when relevant 3. **Resources**: Additional files load only when referenced Keep file references one level deep from `SKILL.md`. ## Slash Command Behavior Skills and prompt files both appear after typing `/` in chat. | Configuration | Slash command | Auto-loaded | |---|---|---| | Default (both omitted) | Yes | Yes | | `user-invocable: false` | No | Yes | | `disable-model-invocation: true` | Yes | No | | Both set | No | No | ## When to Use Repeatable, on-demand workflows with bundled assets (scripts, templates, reference docs). ## Core Principles 1. **Keyword-rich descriptions**: Include trigger words for discovery 2. **Progressive loading**: Keep SKILL.md under 500 lines; use reference files 3. **Relative paths**: Always use `./` for skill resources 4. **Self-contained**: Include all procedural knowledge to complete the task ## Anti-patterns - **Vague descriptions**: "A helpful skill" doesn't enable discovery - **Monolithic SKILL.md**: Everything in one file instead of references - **Name mismatch**: Folder name doesn't match `name` field - **Missing procedures**: Descriptions without step-by-step guidance ================================================ FILE: assets/prompts/skills/agent-customization/references/workspace-instructions.md ================================================ # [Workspace Instructions](https://code.visualstudio.com/docs/copilot/customization/custom-instructions) Guidelines that automatically apply to all chat requests across your entire workspace. ## File Types (Choose One) | File | Location | Purpose | |------|----------|---------| | `copilot-instructions.md` | `.github/` | Project-wide standards (recommended, cross-editor) | | `AGENTS.md` | Root or subfolders | Open standard, monorepo hierarchy support | Use **only one**—not both. ## AGENTS.md Hierarchy For monorepos, the closest file in the directory tree takes precedence: ``` /AGENTS.md # Root defaults /frontend/AGENTS.md # Frontend-specific (overrides root) /backend/AGENTS.md # Backend-specific (overrides root) ``` Use nested `AGENTS.md` files for monorepos when different areas need different defaults. ## Template Only include sections the workspace benefits from: ```markdown # Project Guidelines ## Code Style {Language and formatting preferences—reference key files that exemplify patterns} ## Architecture {Major components, service boundaries, the "why" behind structural decisions} ## Build and Test {Commands to install, build, test—agents will attempt to run these} ## Conventions {Patterns that differ from common practices—include specific examples} ``` For large repos, link to detailed docs instead of embedding: `See docs/TESTING.md for test conventions.` ## When to Use - General coding standards that apply everywhere - Team preferences shared through version control - Project-wide requirements (testing, documentation) ## Core Principles 1. **Minimal by default**: Only what's relevant to *every* task 2. **Concise and actionable**: Every line should guide behavior 3. **Link, don't embed**: Reference docs instead of copying content. Search for existing docs (`docs/**/*.md`, `CONTRIBUTING.md`, etc.) and catalog what they cover—only inline agent-critical gotchas not documented elsewhere 4. **Keep current**: Update when practices change ## Anti-patterns - **Using both file types**: Having both `copilot-instructions.md` and `AGENTS.md` - **Kitchen sink**: Everything instead of what matters most - **Duplicating docs**: Copying README instead of linking - **Obvious instructions**: Conventions already enforced by linters ================================================ FILE: assets/prompts/skills/get-search-view-results/SKILL.md ================================================ --- name: get-search-view-results description: 'Get the current search results from the Search view in VS Code' --- # Getting Search View Results 1. VS Code has a search view, and it can have existing search results. 2. To get the current search results, you can use the VS Code command `search.action.getSearchResults`. 3. Run that command via the `copilot_runVscodeCommand` tool. Make sure to pass the `skipCheck` argument as true to avoid checking if the command exists, as we know it does. ================================================ FILE: assets/prompts/skills/install-vscode-extension/SKILL.md ================================================ --- name: install-vscode-extension description: 'How to install a VS Code extension from an extension ID. Useful when the user wants to add new capabilities to their VS Code environment by installing extensions.' --- # Installing VS Code extensions 1. VS Code extensions are identified by their unique extension ID, which typically follows the format `publisher.extensionName`. For example, the Python extension by Microsoft has the ID `ms-python.python`. 2. To install a VS Code extension, you need to use the VS Code command `workbench.extensions.installExtension` and pass in the extension ID. The args are of the format: ``` [extensionId, { enable: true, installPreReleaseVersion: boolean }] ``` > NOTE: install the pre-release version of the extension if the user explicitly mentions it or if the current environment is VS Code Insiders. Otherwise, install the stable version. 3. Run that command via the `copilot_runVscodeCommand` tool. Make sure to pass the `skipCheck` argument as true to avoid checking if the command exists, as we know it does. ================================================ FILE: assets/prompts/skills/project-setup-info-context7/SKILL.md ================================================ --- name: project-setup-info-context7 description: "Comprehensive setup steps to help the user create complete project structures in a VS Code workspace. This tool is designed for full project initialization and scaffolding, not for creating individual files. When to use this tool: when the user wants to create a new complete project from scratch; when setting up entire project frameworks (TypeScript projects, React apps, Node.js servers, etc.); when initializing Model Context Protocol (MCP) servers with full structure; when creating VS Code extensions with proper scaffolding; when setting up Next.js, Vite, or other framework-based projects; when the user asks for \"new project\", \"create a workspace\", or \"set up a [framework] project\"; when you need to establish a complete development environment with dependencies, config files, and folder structure. When NOT to use this tool: when creating single files or small code snippets; when adding individual files to existing projects; when making modifications to existing codebases; when the user asks to \"create a file\" or \"add a component\"; for simple code examples or demonstrations; for debugging or fixing existing code. This tool provides complete project setup including folder structure creation, package.json and dependency management, configuration files (tsconfig, eslint, etc.), initial boilerplate code, development environment setup, and build and run instructions. Use other file creation tools for individual files within existing projects." --- # How to setup a project using context7 tools Use context7 tools to find the latest libraries, APIs, and documentation to help the user create and customize their project. Only setup a project if the folder is empty or if you've just called the tool first calling the tool to create a workspace. 1. Call the `mcp_context7_resolve-library-id` tool with your project requirements. 2. Call the `mcp_context7_get-library-docs` tool to get scaffolding instructions. ================================================ FILE: assets/prompts/skills/project-setup-info-local/SKILL.md ================================================ --- name: project-setup-info-local description: 'Comprehensive setup steps to help the user create complete project structures in a VS Code workspace; this tool is designed for full project initialization and scaffolding, not for creating individual files. When to use this tool: user wants to create a new complete project from scratch; setting up entire project frameworks (TypeScript projects, React apps, Node.js servers, etc.); initializing Model Context Protocol (MCP) servers with full structure; creating VS Code extensions with proper scaffolding; setting up Next.js, Vite, or other framework-based projects; user asks for "new project", "create a workspace", "set up a [framework] project"; need to establish a complete development environment with dependencies, config files, and folder structure. When NOT to use this tool: creating single files or small code snippets; adding individual files to existing projects; making modifications to existing codebases; user asks to "create a file" or "add a component"; simple code examples or demonstrations; debugging or fixing existing code. This tool provides complete project setup including: folder structure creation; package.json and dependency management; configuration files (tsconfig, eslint, etc.); initial boilerplate code; development environment setup; build and run instructions. Use other file creation tools for individual files within existing projects.' --- # How to setup a project Determine what kind of project the user wants to create, then based on that, choose which setup info below to follow. Only setup a project if the folder is empty or if you've just called the tool first calling the tool to create a workspace. ## vscode-extension A template for creating a VS Code extension using Yeoman and Generator-Code. Run this command: ``` npx --package yo --package generator-code -- yo code . --skipOpen ``` The command has the following arguments: - `-t, --extensionType`: Specify extension type: ts, js, command-ts, command-js, colortheme, language, snippets, keymap, extensionpack, localization, commandweb, notebook. Defaults to `ts` - `-n, --extensionDisplayName`: Set the display name of the extension. - `--extensionId`: Set the unique ID of the extension. Do not select this option if the user has not requested a unique ID. - `--extensionDescription`: Provide a description for the extension. - `--pkgManager`: Specify package manager: npm, yarn, or pnpm. Defaults to `npm`. - `--bundler`: Bundle the extension using webpack or esbuild. - `--gitInit`: Initialize a Git repository for the extension. - `--snippetFolder`: Specify the location of the snippet folder. - `--snippetLanguage`: Set the language for snippets. ### Rules 1. Do not remove any arguments from the command. Only add arguments if the user requests them. 2. Call the tool `get_vscode_api` with the user's query to get the relevant references. 3. After the tool `get_vscode_api` has completed, only then begin to modify the project. ## next-js A React based framework for building server-rendered web applications. Run this command: ``` npx create-next-app@latest . ``` The command has the following arguments: - `--ts, --typescript`: Initialize as a TypeScript project. This is the default. - `--js, --javascript`: Initialize as a JavaScript project. - `--tailwind`: Initialize with Tailwind CSS config. This is the default. - `--eslint`: Initialize with ESLint config. - `--app`: Initialize as an App Router project. - `--src-dir`: Initialize inside a 'src/' directory. - `--turbopack`: Enable Turbopack by default for development. - `--import-alias `: Specify import alias to use.(default is "@/*") - `--api`: Initialize a headless API using the App Router. - `--empty`: Initialize an empty project. - `--use-npm`: Explicitly tell the CLI to bootstrap the application using npm. - `--use-pnpm`: Explicitly tell the CLI to bootstrap the application using pnpm. - `--use-yarn`: Explicitly tell the CLI to bootstrap the application using Yarn. - `--use-bun`: Explicitly tell the CLI to bootstrap the application using Bun. ## vite A front end build tool for web applications that focuses on speed and performance. Can be used with React, Vue, Preact, Lit, Svelte, Solid, and Qwik. Run this command: ``` npx create-vite@latest . ``` The command has the following arguments: - `-t, --template NAME`: Use a specific template. Available templates: vanilla-ts, vanilla, vue-ts, vue, react-ts, react, react-swc-ts, react-swc, preact-ts, preact, lit-ts, lit, svelte-ts, svelte, solid-ts, solid, qwik-ts, qwik ## mcp-server A Model Context Protocol (MCP) server project. This project supports multiple programming languages including TypeScript, JavaScript, Python, C#, Java, and Kotlin. ### Rules 1. First, visit https://github.com/modelcontextprotocol to find the correct SDK and setup instructions for the requested language. Default to TypeScript if no language is specified. 2. Use the `fetch_webpage` tool to find the correct implementation instructions from https://modelcontextprotocol.io/llms-full.txt 3. Update the copilot-instructions.md file in the .github directory to include references to the SDK documentation 4. Create an `mcp.json` file in the `.vscode` folder in the project root with the following content: `{ "servers": { "mcp-server-name": { "type": "stdio", "command": "command-to-run", "args": [list-of-args] } } }`. - mcp-server-name: The name of the MCP server. Create a unique name that reflects what this MCP server does. - command-to-run: The command to run to start the MCP server. This is the command you would use to run the project you just created. - list-of-args: The arguments to pass to the command. This is the list of arguments you would use to run the project you just created. 5. Install any required VS Code extensions based on the chosen language (e.g., Python extension for Python projects). 6. Inform the user that they can now debug this MCP server using VS Code. ## python-script A simple Python script project which should be chosen when just a single script wants to be created. Required extensions: `ms-python.python`, `ms-python.vscode-python-envs` ### Rules 1. Call the tool `copilot_runVscodeCommand` to correctly create a new Python script project in VS Code. Call the command with the following arguments. Note that "python-script" and "true" are constants while "New Project Name" and "/path/to/new/project" are placeholders for the project name and path respectively. ```json { "name": "python-envs.createNewProjectFromTemplate", "commandId": "python-envs.createNewProjectFromTemplate", "args": ["python-script", "true", "New Project Name", "/path/to/new/project"] } ``` ## python-package A Python package project which can be used to create a distributable package. Required extensions: `ms-python.python`, `ms-python.vscode-python-envs` ### Rules 1. Call the tool `run_vscode_command` to correctly create a new Python package project in VS Code. Call the command with the following arguments. Note that "python-package" and "true" are constants while "New Package Name" and "/path/to/new/project" are placeholders for the package name and path respectively. ```json { "name": "python-envs.createNewProjectFromTemplate", "commandId": "python-envs.createNewProjectFromTemplate", "args": ["python-package", "true", "New Package Name", "/path/to/new/project"] } ``` ================================================ FILE: assets/prompts/skills/troubleshoot/SKILL.md ================================================ --- name: troubleshoot description: Investigate unexpected chat agent behavior by analyzing direct debug logs in JSONL files. Use when users ask why something happened, why a request was slow, why tools or subagents were used or skipped, or why instructions/skills/agents did not load. --- # Troubleshoot ## Purpose This skill investigates and explains unexpected chat agent behavior using direct log files. Use this skill for questions like: - Why did this request take so long? - Why was a tool or subagent called? - Why did instruction/skill/agent files not load? - Why was a tool call blocked or failed? - Why did the model not follow expectations? Base conclusions on evidence from logs. Do not guess. ## Data Source {{DEBUG_LOG_RUNTIME_CONTEXT}} Use direct debug log files written by Copilot Chat: ``` debug-logs// main.jsonl — always start here; primary conversation log runSubagent--.jsonl — (optional) subagent's tool calls & LLM requests searchSubagent-.jsonl — (optional) search subagent work title-.jsonl — (optional, UI-only) title generation categorization-.jsonl — (optional, UI-only) prompt categorization summarize-.jsonl — (optional, UI-only) conversation summarization ``` Always read `main.jsonl` first — it has the full conversation flow. Child files only appear when those operations occurred. `main.jsonl` contains `child_session_ref` entries that link to each child file by name. Title, categorization, and summarize files are UI housekeeping and rarely relevant to troubleshooting. Each line is a JSON object. Common fields: `ts` (epoch ms), `dur` (duration ms), `sid` (session ID), `type`, `name`, `spanId`, `parentSpanId`, `status` (`ok`|`error`), `attrs` (type-specific details). ### Event Type Reference with Examples #### discovery — customization file loading (instructions, skills, agents, hooks) ```jsonl {"ts":1773200251309,"dur":0,"sid":"62f52dec","type":"discovery","name":"Load Instructions","spanId":"2cb1f2f4","status":"ok","attrs":{"details":"Resolved 0 instructions in 0.0ms | folders: [/c:/Users/user/.copilot/instructions, /workspace/.github/instructions]","category":"discovery","source":"core"}} {"ts":1773200251415,"dur":0,"sid":"62f52dec","type":"discovery","name":"Load Agents","spanId":"38a897d8","status":"ok","attrs":{"details":"Resolved 3 agents in 0.0ms | loaded: [Plan, Ask, Explore] | folders: [/workspace/.github/agents]","category":"discovery","source":"core"}} {"ts":1773200251431,"dur":0,"sid":"62f52dec","type":"discovery","name":"Load Skills","spanId":"472eb225","status":"ok","attrs":{"details":"Resolved 6 skills in 0.0ms | loaded: [agent-customization, troubleshoot, ...]","category":"discovery","source":"core"}} ``` Key attrs: `details` (human-readable summary with folder paths, loaded items, skip reasons), `category` (always `"discovery"`), `source` (`"core"`). #### tool_call — tool invocation (success or failure) ```jsonl {"ts":1773200222647,"dur":4,"sid":"62f52dec","type":"tool_call","name":"manage_todo_list","spanId":"000000000000000b","parentSpanId":"0000000000000003","status":"ok","attrs":{"args":"{\"operation\":\"read\"}","result":"No todo list found."}} {"ts":1773200234047,"dur":8937,"sid":"62f52dec","type":"tool_call","name":"run_in_terminal","spanId":"000000000000000d","parentSpanId":"0000000000000003","status":"error","attrs":{"args":"{\"command\":\"echo rama\"}","result":"ERROR: conpty.node missing","error":"A native exception occurred during launch"}} ``` Key attrs: `args` (JSON string of tool input), `result` (tool output or error text), `error` (present when `status:"error"`). #### llm_request — model round-trip ```jsonl {"ts":1773200231010,"dur":3001,"sid":"62f52dec","type":"llm_request","name":"chat:gpt-4o","spanId":"000000000000000c","parentSpanId":"0000000000000003","status":"ok","attrs":{"model":"gpt-4o","inputTokens":15025,"outputTokens":126,"ttft":1987}} ``` Key attrs: `model`, `inputTokens`, `outputTokens`, `ttft` (time to first token in ms), `error` (when failed). #### agent_response — model output (text + tool calls) ```jsonl {"ts":1773200234011,"dur":0,"sid":"62f52dec","type":"agent_response","name":"agent_response","spanId":"agent-msg-000000000000000c","parentSpanId":"0000000000000003","status":"ok","attrs":{"response":"[{\"role\":\"assistant\",\"parts\":[{\"type\":\"text\",\"content\":\"Running your command now.\"},{\"type\":\"tool_call\",\"name\":\"run_in_terminal\",\"arguments\":\"{...}\"}]}]"}} ``` Key attrs: `response` (JSON-encoded array of message parts; may be truncated). #### user_message — user input ```jsonl {"ts":1773200251345,"dur":0,"sid":"62f52dec","type":"user_message","name":"user_message","spanId":"000000000000000f","status":"ok","attrs":{"content":"using subagent count .md"}} ``` Key attrs: `content` (the user's message text). #### subagent — subagent invocation ```jsonl {"ts":1773200254954,"dur":7921,"sid":"62f52dec","type":"subagent","name":"Explore","spanId":"0000000000000014","parentSpanId":"0000000000000013","status":"ok","attrs":{"agentName":"Explore"}} ``` Key attrs: `agentName`, `description` (optional), `error` (when failed). #### generic — miscellaneous events ```jsonl {"ts":1773200260000,"dur":0,"sid":"62f52dec","type":"generic","name":"some-event","spanId":"abc123","status":"ok","attrs":{"details":"Additional context","category":"some-category"}} ``` ### Reading the event hierarchy Events form a tree via `spanId`/`parentSpanId`. A typical chain: 1. `user_message` (spanId: `X`) — the user's turn 2. `llm_request` (parentSpanId: `X`) — model call for that turn 3. `agent_response` (parentSpanId: `X`) — what the model returned 4. `tool_call` (parentSpanId: `X`) — tool executed from the response 5. Another `llm_request` (parentSpanId: `X`) — next model call after tool result Subagent calls create nested hierarchies: the `tool_call` for `runSubagent` (spanId: `Y`) becomes the parent for a child `subagent` span, which in turn parents its own `llm_request`/`tool_call` events. ## Tooling Strategy (important) Debug log files live outside the workspace (in user storage), so workspace-scoped search tools like `grep_search` cannot access them. Use the terminal instead. **Do not use `grep_search` for log files — it only works on workspace files.** ### macOS / Linux / WSL / Git Bash Use `run_in_terminal` with `grep` or `jq`: - Find errors: `grep '"status":"error"' ` - Find discovery events: `grep '"type":"discovery"' ` - Find slow events (duration > 5s): `jq -c 'select(.dur > 5000)' ` - Find tool calls: `grep '"type":"tool_call"' ` - Search for specific text: `grep 'search_term' ` - Get last N lines: `tail -n 50 ` - Count events by type: `jq -r '.type' | sort | uniq -c | sort -rn` - Extract specific fields: `jq -c '{type, name, status, dur}' ` - Filter by type and show details: `jq -c 'select(.type == "discovery")' ` - Find user messages: `jq -c 'select(.type == "user_message") | .attrs.content' ` ### Windows (PowerShell) Use `run_in_terminal` with PowerShell commands: - Find errors: `Select-String '"status":"error"' ` - Find discovery events: `Select-String '"type":"discovery"' ` - Find tool calls: `Select-String '"type":"tool_call"' ` - Search for specific text: `Select-String 'search_term' ` - Get last N lines: `Get-Content -Tail 50` - Parse and filter with Node.js (always available): `node -e "require('fs').readFileSync('','utf8').split('\n').filter(Boolean).map(JSON.parse).filter(e => e.dur > 5000).forEach(e => console.log(JSON.stringify(e)))"` - Count events by type: `node -e "const lines=require('fs').readFileSync('','utf8').split('\n').filter(Boolean).map(JSON.parse);const c={};lines.forEach(e=>c[e.type]=(c[e.type]||0)+1);Object.entries(c).sort((a,b)=>b[1]-a[1]).forEach(([t,n])=>console.log(n,t))"` ### General rules - **Log files can be very large** (tens of MB or more for long sessions). Always check file size first if unsure: `ls -lh ` (or `(Get-Item ).Length` on Windows). If the file is large, avoid commands that load the entire file into memory (e.g. `node -e` with `readFileSync`). Prefer streaming tools like `grep`, `jq`, `Select-String`, `tail`, or `head`. - Use `read_file` only for small targeted ranges (a few lines) once you know the line numbers. Never read entire log files. - Use `run_in_terminal` with `ls -lh` (or `dir` on Windows) to locate candidate `.jsonl` files and check their sizes. - On Windows, if `grep`/`jq` are not available, fall back to `Select-String` or `node -e` one-liners (only for smaller files). ## Investigation Workflow 1. Identify the log file - **Focus on the current session log directory first.** The current session log directory is provided in the Runtime Log Context section above. Start by reading `main.jsonl` in that directory unless the user explicitly asks about a different or older session. - Only search other session files in the debug-logs directory if the issue spans multiple sessions or the current session log doesn't contain relevant events. 2. Triage quickly via `run_in_terminal` (use `grep`/`jq` on macOS/Linux, `Select-String`/`node -e` on Windows) - Errors: search for `"status":"error"` - Latency: filter for high `dur` values (> 5000) - Discovery issues: search for `"type":"discovery"` - Tool behavior: search for `"type":"tool_call"` - Model behavior: search for `"type":"llm_request"` 3. Read only relevant slices - Pull exact lines around suspicious events. - Correlate with `spanId` / `parentSpanId` when needed. 4. Determine root cause - Pick the most likely cause from evidence. - If multiple factors contribute, order by impact. 5. Provide remediation - Offer concrete next steps when possible. ## Network Issue Investigation If you suspect network connectivity or authentication problems (e.g., repeated request timeouts, 401/403 errors, or model endpoint failures in the logs), run the VS Code command `github.copilot.debug.collectDiagnostics` using the `run_vscode_command` tool. The command returns the full diagnostics report as a string, so you can read the result directly from the tool output. The report includes: - Authentication and token status - Network reachability checks - Proxy and certificate configuration - Extension and environment details The command also opens the report in an editor for the user to see. Use the returned string to diagnose the network issue. ## Customization Documentation Reference When investigating issues related to a specific type of customization file (instructions, prompt files, agents, etc.) and you need more details about the expected format or behavior, load the relevant documentation page: - Custom instructions: `https://code.visualstudio.com/docs/copilot/customization/custom-instructions` - Prompt files: `https://code.visualstudio.com/docs/copilot/customization/prompt-files` - Custom agents: `https://code.visualstudio.com/docs/copilot/customization/custom-agents` - Language models: `https://code.visualstudio.com/docs/copilot/customization/language-models` - MCP servers: `https://code.visualstudio.com/docs/copilot/customization/mcp-servers` - Hooks: `https://code.visualstudio.com/docs/copilot/customization/hooks` - Agent plugins: `https://code.visualstudio.com/docs/copilot/customization/agent-plugins` Use these when you need to verify file format expectations, confirm supported fields, or help the user fix a customization file. ## Last Resort — Copilot Issues Wiki When your investigation yields no clear root cause or you have no specific remediation suggestions: 1. Load the Copilot Issues wiki page: `https://github.com/microsoft/vscode/wiki/Copilot-Issues`. 2. Search the returned wiki content for sections relevant to the user's problem. 3. Summarize the applicable troubleshooting steps from the wiki in your response. 4. If the wiki contains relevant guidance, present those steps as concrete suggestions the user can try. 5. If even the wiki has no relevant information, tell the user: "The diagnostics logs do not show a clear cause for this behavior, and the known issues wiki does not cover this scenario. Consider filing an issue at https://github.com/microsoft/vscode/issues." ## Response Guidelines Your response should cover: - What happened and why (the root cause or most likely explanation) - Key evidence from logs (paraphrased, not raw dumps) - How to fix it or what to try next You do not need separate headers for each of these. For straightforward issues, a short combined explanation with a "How to fix" section is fine. Use more structure (headers, multiple sections) only when the issue is complex or involves multiple contributing factors. ### Formatting - Use headers, bullet points, and bold text to make the response scannable. - Keep paragraphs short. Prefer lists over walls of text. - When citing log evidence, paraphrase or summarize rather than pasting raw log lines. If a specific detail is important (e.g., an error message), quote just that message — not entire log entries. - Use tables when comparing multiple values or events (e.g., a name mismatch, latency breakdown across steps). ### Abstraction level - Do not narrate your investigation process. Never say things like "I'm investigating the session debug log…", "I found the key clue in the log…", or "I'm now checking the skill file metadata…". Jump straight to the findings. - Do not use internal terminology like "discovery summary", "frontmatter name", "the loader", "event hierarchy", or "span tree". Describe what happened in plain language the user can act on. - Do not describe the internal log file structure, event types, or JSONL format to the user — they do not need to know these implementation details. - Refer to log files abstractly (e.g., "the debug log" or "the session log") rather than by literal filenames like `main.jsonl` or `runSubagent-Explore-abc123.jsonl`. - Focus on what happened and why, not how you found it. ### Example **Bad** (narrates investigation, uses internal terms, pastes raw log): > I'm investigating the session debug log to confirm whether a testing skill was discovered. In the skills discovery summary, the loader reports: `skipped: testing2 (name-mismatch)`. The frontmatter name in SKILL.md doesn't match the folder identity. **Good** (concise, user-friendly, actionable): > The "testing" skill was found but not loaded because there's a name mismatch between the folder and the skill file: > > | | Value | > |---|---| > | **Folder name** | `testing` | > | **Name in SKILL.md** | `testing2` | > > These must match for the skill to load. > > **How to fix** > > Either: > - Change `name: testing2` to `name: testing` in your SKILL.md, **or** > - Rename the folder from `testing/` to `testing2/` > > Then start a new chat session so it gets picked up. ## Important Rules - Never assume causality without evidence. - Use `run_in_terminal` to search log files — never use `grep_search` (it cannot access files outside the workspace). Use `grep`/`jq` on macOS/Linux, `Select-String`/`node -e` on Windows. - Never read entire log files with `read_file` — they can be very large. Search first, then `read_file` for small targeted ranges. - Keep log access targeted and efficient. - If you suspect network issues, run `github.copilot.debug.collectDiagnostics` via the `run_vscode_command` tool and use the returned diagnostics string before concluding. - If no clear cause is found, consult the Copilot Issues wiki before giving up. If even the wiki has no relevant information, say so explicitly. ================================================ FILE: build/.cachesalt ================================================ 2023-12-30T08:16:00.983Z ================================================ FILE: build/listBuildCacheFiles.js ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ //@ts-check 'use strict'; const fs = require('fs'); const path = require('path'); if (process.argv.length !== 3) { console.error('Usage: node listBuildCacheFiles.js OUTPUT_FILE'); process.exit(-1); } const ROOT = path.join(__dirname, '../'); /** * @param {string} location * @param {string[]} result */ function listAllFiles(location, result) { const entries = fs.readdirSync(path.join(ROOT, location)); for (const entry of entries) { const entryPath = `${location}/${entry}`; /** @type {import('fs').Stats} */ let stat; try { stat = fs.statSync(path.join(ROOT, entryPath)); } catch (err) { continue; } if (stat.isDirectory()) { listAllFiles(entryPath, result); } else { result.push(entryPath); } } } /** @type {string[]} */ const result = []; listAllFiles('node_modules', result); // node modules listAllFiles('dist', result); // contains wasm files fs.writeFileSync(process.argv[2], result.join('\n') + '\n'); ================================================ FILE: build/npm-package.yml ================================================ trigger: batch: true branches: include: - main tags: include: - v* pr: [main] resources: repositories: - repository: templates type: github name: microsoft/vscode-engineering ref: main endpoint: Monaco parameters: - name: nextVersion displayName: '🚀 Release Version (eg: none, major, minor, patch, prerelease, or X.X.X)' type: string default: 'none' name: "$(Date:yyyyMMdd).$(Rev:r)${{ replace(format(' (🚀 {0})', parameters.nextVersion), ' (🚀 none)', '') }}" extends: template: azure-pipelines/npm-package/pipeline.yml@templates parameters: npmPackages: - name: vscode-copilot-chat buildSteps: - task: NodeTool@0 inputs: versionSpec: 22.x displayName: 🛠 Install Node.js (22.x) - bash: npm ci && npm run extract-chat-lib && rm -rf node_modules displayName: 📂 Extract chat-lib - script: npm ci displayName: 📦 Install chat-lib dependencies workingDirectory: chat-lib - script: npm run build displayName: 🔨 Build chat-lib workingDirectory: chat-lib testPlatforms: - name: Linux nodeVersions: [22.x] - name: MacOS nodeVersions: [22.x] - name: Windows nodeVersions: [22.x] workingDirectory: chat-lib testSteps: - bash: npm ci && npm run extract-chat-lib && rm -rf node_modules displayName: 📂 Extract chat-lib - script: npm ci displayName: 📦 Install chat-lib dependencies workingDirectory: chat-lib - script: npm run build displayName: 🔨 Build chat-lib workingDirectory: chat-lib - script: npm test displayName: 🧪 Run chat-lib tests workingDirectory: chat-lib # Tag-triggered: date-stamped patch (e.g., v0.40.2026031601) → publish to next ${{ if and(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), gt(length(variables['Build.SourceBranchName']), 13)) }}: publishPackage: true publishRequiresApproval: false nextVersion: ${{ replace(variables['Build.SourceBranchName'], 'v', '') }} tag: next # Tag-triggered: short patch (e.g., v0.39.1) → publish to latest ${{ elseif startsWith(variables['Build.SourceBranch'], 'refs/tags/v') }}: publishPackage: true publishRequiresApproval: false nextVersion: ${{ replace(variables['Build.SourceBranchName'], 'v', '') }} ${{ elseif eq(parameters.nextVersion, 'none') }}: publishPackage: false # Manual prerelease → publish to next ${{ elseif eq(parameters.nextVersion, 'prerelease') }}: publishPackage: true publishRequiresApproval: false nextVersion: prerelease tag: next ${{ else }}: publishPackage: true nextVersion: ${{ parameters.nextVersion }} ghCreateRelease: false ghReleaseAddChangeLog: false ================================================ FILE: build/pr-check-cache-files.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); type Commit = { readonly sha: string; readonly committer: { readonly login: string; }; readonly commit: { readonly verification: { readonly verified: boolean; readonly reason: string; }; }; readonly files: readonly PullRequestFile[]; } type PullRequestFile = { readonly filename: string; readonly status: string; } type PullRequestCommit = { readonly sha: string; } const collaborators = [ "aeschli", "aiday-mar", "alexdima", "alexr00", "amunger", "anthonykim1", "bamurtaugh", "benibenj", "benvillalobos", "bhavyaus", "binderjoe", "bpasero", "bryanchen-d", "burkeholland", "chrmarti", "connor4312", "cwebster-99", "dbaeumer", "deepak1556", "devinvalenciano", "digitarald", "dileepyavan", "dineshc-msft", "dmitrivMS", "DonJayamanne", "egamma", "eleanorjboyd", "eli-w-king", "hawkticehurst", "hediet", "isidorn", "jo-oikawa", "joaomoreno", "joshspicer", "jrieken", "jruales", "justschen", "karthiknadig", "kieferrm", "kkbrooks", "kycutler", "lramos15", "lszomoru", "luabud", "meganrogge", "minsa110", "mjbvz", "mrleemurray", "nguyenchristy", "ntrogh", "olguzzar", "osortega", "pierceboggan", "pwang347", "rebornix", "roblourens", "rzhao271", "sandy081", "sbatten", "TylerLeonhardt", "Tyriar", "ulugbekna", "vijayupadya", "Yoyokrazy" ]; // TODO@lszomoru - Investigate issues with the `/collaborators` endpoint // async function getCollaborators(repository: string): Promise { // const { stdout, stderr } = await execAsync( // `gh api -H "Accept: application/vnd.github+json" /repos/${repository}/collaborators --paginate`, { maxBuffer: 25 * 1024 * 1024 }); // if (stderr) { // throw new Error(`Error fetching repository collaborators - ${stderr}`); // } // return JSON.parse(stdout) as ReadonlyArray; // } async function getCommit(repository: string, sha: string): Promise { const { stdout, stderr } = await execAsync( `gh api -H "Accept: application/vnd.github+json" /repos/${repository}/commits/${sha}`, { maxBuffer: 25 * 1024 * 1024 }); if (stderr) { throw new Error(`Error fetching commit ${sha} - ${stderr}`); } return JSON.parse(stdout) as Commit; } async function getPullRequestFiles(repository: string, pullRequestNumber: string): Promise { const { stdout, stderr } = await execAsync( `gh api -H "Accept: application/vnd.github+json" /repos/${repository}/pulls/${pullRequestNumber}/files --paginate`, { maxBuffer: 25 * 1024 * 1024 }); if (stderr) { throw new Error(`Error fetching pull request files - ${stderr}`); } return JSON.parse(stdout) as readonly PullRequestFile[]; } async function getPullRequestCommits(repository: string, pullRequestNumber: string): Promise { const { stdout, stderr } = await execAsync( `gh api -H "Accept: application/vnd.github+json" /repos/${repository}/pulls/${pullRequestNumber}/commits --paginate`, { maxBuffer: 25 * 1024 * 1024 }); if (stderr) { throw new Error(`Error fetching pull request commits - ${stderr}`); } return JSON.parse(stdout).map((commit: PullRequestCommit) => commit.sha); } async function checkDatabaseFile(files: ReadonlyArray): Promise { const baseFile = files.find(f => f.filename.toLowerCase() === 'test/simulation/cache/base.sqlite'); if (!baseFile) { console.log('✅ Pull request does not contain the base file.'); return true; } const statusCheck = baseFile.status === 'modified'; console.log(`🔍 Pull request contains the base file. Checking status...`); console.log(` - 🗄️ ${baseFile.filename}; Status: ${baseFile.status} ${statusCheck ? '✅' : '⛔'}`); return statusCheck; } async function checkDatabaseLayerFiles(repository: string, pullRequestNumber: string, files: readonly PullRequestFile[]) : Promise<{ statusCheck: boolean; verifiedCheck: boolean; collaboratorCheck: boolean }> { const layerFiles = files.filter(f => f.filename.toLowerCase().startsWith('test/simulation/cache/layers/')); if (layerFiles.length === 0) { console.log('✅ Pull request does not contain any layer files.'); return { statusCheck: true, verifiedCheck: true, collaboratorCheck: true }; } // Get collaborators and commits for the pull request // const collaborators = await getCollaborators(repository); const pullRequestCommits = await getPullRequestCommits(repository, pullRequestNumber); const commitsWithDetails = await Promise.all(pullRequestCommits.map(sha => getCommit(repository, sha))); let statusCheckResult = true, verifiedCheckResult = true, collaboratorCheckResult = true; console.log(`🔍 Pull request contains ${layerFiles.length} layer files. Checking status and author...`); for (const file of layerFiles) { const statusCheck = file.status === 'added' || file.status === 'removed'; console.log(` - 🗄️ ${file.filename}`); console.log(` - Status: ${file.status} ${statusCheck ? '✅' : '⛔'}`); if (!statusCheck) { statusCheckResult = false; } // List of commits that contain the file const commits = commitsWithDetails.filter(c => c.files.some(f => f.filename === file.filename)); console.log(` - Commit(s):`); for (const commit of commits) { const collaboratorCheck = collaborators.find(c => c === commit.committer.login); const verifiedCheck = commit.commit.verification.verified && commit.commit.verification.reason === 'valid'; console.log(` - ${commit.sha} by ${commit.committer.login}. Collaborator: ${collaboratorCheck ? '✅' : '⛔'} Verified: ${verifiedCheck ? '✅' : '⛔'}`); if (!verifiedCheck) { verifiedCheckResult = false; } if (!collaboratorCheck) { collaboratorCheckResult = false; } } } return { statusCheck: statusCheckResult, verifiedCheck: verifiedCheckResult, collaboratorCheck: collaboratorCheckResult }; } async function main() { try { const repository = process.env['REPOSITORY']; const pullRequestNumber = process.env['PULL_REQUEST']; if (!repository || !pullRequestNumber) { throw new Error('Missing required environment variables: REPOSITORY or PULL_REQUEST'); } console.log(`🔍 Checking pull request #${pullRequestNumber} in repository "${repository}"...`); // Get a list of files in the pull request const files = await getPullRequestFiles(repository, pullRequestNumber); // 1. Check base file status const baseCheckResult = await checkDatabaseFile(files); // 2. Check cache layer file(s) status and author const layerCheckResult = await checkDatabaseLayerFiles(repository, pullRequestNumber, files); if (!baseCheckResult) { throw new Error('Base file can only be modified in a pull request.'); } if (!layerCheckResult.statusCheck) { throw new Error('Cache layer files can only be added or deleted, never modified'); } if (!layerCheckResult.verifiedCheck || !layerCheckResult.collaboratorCheck) { throw new Error('Cache layer files can only be added by VS Code team members with signed commits'); } } catch (error) { console.log('::error::⛔', error); process.exit(1); } } if (require.main === module) { main(); } ================================================ FILE: build/pre-release.yml ================================================ # Run on a schedule trigger: none pr: none schedules: - cron: '0 4 * * Mon-Fri' displayName: Mon-Fri at 4:00 UTC branches: include: - main - cron: '0 16 * * Mon-Fri' displayName: Mon-Fri at 16:00 UTC branches: include: - main resources: repositories: - repository: templates type: github name: microsoft/vscode-engineering ref: main endpoint: Monaco parameters: - name: customNPMRegistry displayName: 📦 Custom NPM Registry (Terrapin) type: boolean default: true - name: generateNotice displayName: 📝 Generate Notice type: boolean default: true - name: publishExtension displayName: 🚀 Publish Pre-Release type: boolean default: false - name: processLocalization displayName: 🌐 Process Localization type: boolean default: true extends: template: azure-pipelines/extension/pre-release.yml@templates parameters: l10nSourcePaths: ./src l10nShouldProcess: ${{ parameters.processLocalization }} nodeVersion: 22.21.x standardizedVersioning: true cgIgnoreDirectories: $(Build.SourcesDirectory)/script # Suppress false positive strings SG.default.* that show up in # dist/extension.js after the build. The original strings come from ora. vscePackageArgs: '--allow-package-secrets sendgrid' buildSteps: - task: NodeTool@0 inputs: versionSpec: '22.21.x' - task: AzureKeyVault@2 displayName: "Azure Key Vault: Get Secrets" inputs: azureSubscription: vscode KeyVaultName: vscode-build-secrets SecretsFilter: "github-distro-mixin-password" - pwsh: | "machine github.com`nlogin vscode`npassword $(github-distro-mixin-password)" | Out-File "$Home/_netrc" -Encoding ASCII condition: and(succeeded(), contains(variables['Agent.OS'], 'windows')) displayName: Setup distro auth (Windows) - script: | mkdir -p .build cat << EOF | tee ~/.netrc .build/.netrc > /dev/null machine github.com login vscode password $(github-distro-mixin-password) EOF condition: and(succeeded(), not(contains(variables['Agent.OS'], 'windows'))) displayName: Setup distro auth (non-Windows) - task: Cache@2 inputs: key: '"release_build_cache" | build/.cachesalt | build/setup-emsdk.sh | package-lock.json' path: .build/build_cache cacheHitVar: BUILD_CACHE_RESTORED displayName: Restore build cache (node modules, python packages) - script: ./build/setup-emsdk.sh displayName: Setup emsdk condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) - script: echo '##vso[task.prependpath]/opt/dev/emsdk/upstream/emscripten' displayName: Setup emsdk path 1 condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) - script: echo '##vso[task.prependpath]/opt/dev/emsdk' displayName: Setup emsdk path 2 condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) - script: tar -xzf .build/build_cache/cache.tgz condition: and(succeeded(), eq(variables.BUILD_CACHE_RESTORED, 'true')) displayName: Extract build cache - script: npm ci displayName: Install dependencies condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) - script: | set -e mkdir -p .build node build/listBuildCacheFiles.js .build/build_cache_list.txt mkdir -p .build/build_cache tar -czf .build/build_cache/cache.tgz --files-from .build/build_cache_list.txt condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) displayName: Create build cache archive - pwsh: | # Clone the vscode-capi repository git clone https://github.com/microsoft/vscode-capi.git --depth 1 ../vscode-capi # Run the mixin script Push-Location ../vscode-capi npm ci && npm run mixin # Clean up the cloned repository Pop-Location Remove-Item -Recurse -Force ../vscode-capi displayName: mixin - script: npm run build -- --prerelease displayName: npm run build uploadSourceMaps: enabled: true # testPlatforms: # - name: Linux # nodeVersions: [16.x] # - name: MacOS # nodeVersions: [16.x] # - name: Windows # nodeVersions: [16.x] testSteps: - checkout: self lfs: true - task: NodeTool@0 inputs: versionSpec: '22.x' - task: AzureKeyVault@2 displayName: "Azure Key Vault: Get Secrets" inputs: azureSubscription: vscode KeyVaultName: vscode-build-secrets SecretsFilter: "github-distro-mixin-password" - pwsh: | "machine github.com`nlogin vscode`npassword $(github-distro-mixin-password)" | Out-File "$Home/_netrc" -Encoding ASCII condition: and(succeeded(), contains(variables['Agent.OS'], 'windows')) displayName: Setup distro auth (Windows) - script: | mkdir -p .build cat << EOF | tee ~/.netrc .build/.netrc > /dev/null machine github.com login vscode password $(github-distro-mixin-password) EOF condition: and(succeeded(), not(contains(variables['Agent.OS'], 'windows'))) displayName: Setup distro auth (non-Windows) - task: Cache@2 inputs: key: '"release_build_cache" | build/.cachesalt | build/setup-emsdk.sh | package-lock.json' path: .build/build_cache cacheHitVar: BUILD_CACHE_RESTORED displayName: Restore build cache (node modules, python packages) - script: ./build/setup-emsdk.sh displayName: Setup emsdk condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) - script: echo '##vso[task.prependpath]/opt/dev/emsdk/upstream/emscripten' displayName: Setup emsdk path 1 condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) - script: echo '##vso[task.prependpath]/opt/dev/emsdk' displayName: Setup emsdk path 2 condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) - script: tar -xzf .build/build_cache/cache.tgz condition: and(succeeded(), eq(variables.BUILD_CACHE_RESTORED, 'true')) displayName: Extract build cache - script: npm ci displayName: Install dependencies condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) - script: | set -e mkdir -p .build node build/listBuildCacheFiles.js .build/build_cache_list.txt mkdir -p .build/build_cache tar -czf .build/build_cache/cache.tgz --files-from .build/build_cache_list.txt condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) displayName: Create build cache archive - task: AzureCLI@2 inputs: azureSubscription: 'VS Code Development WIF' scriptType: 'bash' scriptLocation: 'inlineScript' inlineScript: npm run setup displayName: npm run setup - script: npm run setup:dotnet displayName: Install dotnet cli - script: npm run typecheck displayName: npm run typecheck - script: npm run lint displayName: npm run lint - script: npm run compile displayName: npm run compile - script: npm run test:unit displayName: Run vitest unit tests - script: npm run simulate-ci displayName: Run simulation tests - script: xvfb-run -a npm run test:extension displayName: Run extension tests using VS Code - script: npm run test:prompt displayName: Run Completions Core prompt tests - script: xvfb-run -a npm run test:completions-core displayName: Run Completions Core lib tests using VS Code - script: xvfb-run -a npm run test:sanity displayName: Run extension sanity tests using VS Code tsa: config: areaPath: 'Visual Studio Code Copilot Extensions' serviceTreeID: '1788a767-5861-45fb-973b-c686b67c5541' enabled: true ${{ if eq(parameters.customNPMRegistry, false) }}: customNPMRegistry: '' generateNotice: ${{ parameters.generateNotice }} publishExtension: ${{ parameters.publishExtension }} ghReleasePublishVSIX: true ================================================ FILE: build/release.yml ================================================ name: $(Date:yyyyMMdd)$(Rev:.r) trigger: branches: include: - main pr: none resources: repositories: - repository: templates type: github name: microsoft/vscode-engineering ref: main endpoint: Monaco parameters: - name: customNPMRegistry displayName: 📦 Custom NPM Registry (Terrapin) type: boolean default: true - name: generateNotice displayName: 📝 Generate Notice type: boolean default: true - name: publishExtension displayName: 🚀 Publish Stable Extension type: boolean default: false extends: template: azure-pipelines/extension/stable.yml@templates parameters: l10nSourcePaths: ./src nodeVersion: 22.21.x cgIgnoreDirectories: $(Build.SourcesDirectory)/script # Suppress false positive strings SG.default.* that show up in # dist/extension.js after the build. The original strings come from ora. vscePackageArgs: '--allow-package-secrets sendgrid' buildSteps: - task: NodeTool@0 inputs: versionSpec: '22.21.x' - task: AzureKeyVault@2 displayName: "Azure Key Vault: Get Secrets" inputs: azureSubscription: vscode KeyVaultName: vscode-build-secrets SecretsFilter: "github-distro-mixin-password" - pwsh: | "machine github.com`nlogin vscode`npassword $(github-distro-mixin-password)" | Out-File "$Home/_netrc" -Encoding ASCII condition: and(succeeded(), contains(variables['Agent.OS'], 'windows')) displayName: Setup distro auth (Windows) - script: | mkdir -p .build cat << EOF | tee ~/.netrc .build/.netrc > /dev/null machine github.com login vscode password $(github-distro-mixin-password) EOF condition: and(succeeded(), not(contains(variables['Agent.OS'], 'windows'))) displayName: Setup distro auth (non-Windows) - task: Cache@2 inputs: key: '"release_build_cache" | build/.cachesalt | build/setup-emsdk.sh | package-lock.json' path: .build/build_cache cacheHitVar: BUILD_CACHE_RESTORED displayName: Restore build cache (node modules, python packages) - script: ./build/setup-emsdk.sh displayName: Setup emsdk condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) - script: echo '##vso[task.prependpath]/opt/dev/emsdk/upstream/emscripten' displayName: Setup emsdk path 1 condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) - script: echo '##vso[task.prependpath]/opt/dev/emsdk' displayName: Setup emsdk path 2 condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) - script: tar -xzf .build/build_cache/cache.tgz condition: and(succeeded(), eq(variables.BUILD_CACHE_RESTORED, 'true')) displayName: Extract build cache - script: npm ci displayName: Install dependencies condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) - script: | set -e mkdir -p .build node build/listBuildCacheFiles.js .build/build_cache_list.txt mkdir -p .build/build_cache tar -czf .build/build_cache/cache.tgz --files-from .build/build_cache_list.txt condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) displayName: Create build cache archive - pwsh: | # Clone the vscode-capi repository git clone https://github.com/microsoft/vscode-capi.git --depth 1 ../vscode-capi # Run the mixin script Push-Location ../vscode-capi npm ci && npm run mixin # Clean up the cloned repository Pop-Location Remove-Item -Recurse -Force ../vscode-capi displayName: mixin - script: npm run build displayName: npm run build uploadSourceMaps: enabled: true # testPlatforms: # - name: Linux # nodeVersions: [16.x] # - name: MacOS # nodeVersions: [16.x] # - name: Windows # nodeVersions: [16.x] testSteps: - checkout: self lfs: true - task: NodeTool@0 inputs: versionSpec: '22.21.x' - task: AzureKeyVault@2 displayName: "Azure Key Vault: Get Secrets" inputs: azureSubscription: vscode KeyVaultName: vscode-build-secrets SecretsFilter: "github-distro-mixin-password" - pwsh: | "machine github.com`nlogin vscode`npassword $(github-distro-mixin-password)" | Out-File "$Home/_netrc" -Encoding ASCII condition: and(succeeded(), contains(variables['Agent.OS'], 'windows')) displayName: Setup distro auth (Windows) - script: | mkdir -p .build cat << EOF | tee ~/.netrc .build/.netrc > /dev/null machine github.com login vscode password $(github-distro-mixin-password) EOF condition: and(succeeded(), not(contains(variables['Agent.OS'], 'windows'))) displayName: Setup distro auth (non-Windows) - task: Cache@2 inputs: key: '"release_build_cache" | build/.cachesalt | build/setup-emsdk.sh | package-lock.json' path: .build/build_cache cacheHitVar: BUILD_CACHE_RESTORED displayName: Restore build cache (node modules, python packages) - script: ./build/setup-emsdk.sh displayName: Setup emsdk condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) - script: echo '##vso[task.prependpath]/opt/dev/emsdk/upstream/emscripten' displayName: Setup emsdk path 1 condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) - script: echo '##vso[task.prependpath]/opt/dev/emsdk' displayName: Setup emsdk path 2 condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) - script: tar -xzf .build/build_cache/cache.tgz condition: and(succeeded(), eq(variables.BUILD_CACHE_RESTORED, 'true')) displayName: Extract build cache - script: npm ci displayName: Install dependencies condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) - script: | set -e mkdir -p .build node build/listBuildCacheFiles.js .build/build_cache_list.txt mkdir -p .build/build_cache tar -czf .build/build_cache/cache.tgz --files-from .build/build_cache_list.txt condition: and(succeeded(), ne(variables.BUILD_CACHE_RESTORED, 'true')) displayName: Create build cache archive - task: AzureCLI@2 inputs: azureSubscription: 'VS Code Development WIF' scriptType: 'bash' scriptLocation: 'inlineScript' inlineScript: npm run setup displayName: npm run setup - script: npm run setup:dotnet displayName: Install dotnet cli - script: npm run typecheck displayName: npm run typecheck - script: npm run lint displayName: npm run lint - script: npm run compile displayName: npm run compile - script: npm run test:unit displayName: Run vitest unit tests - script: npm run simulate-ci displayName: Run simulation tests - script: xvfb-run -a npm run test:extension displayName: Run extension tests using VS Code - script: npm run test:prompt displayName: Run Completions Core prompt tests - script: xvfb-run -a npm run test:completions-core displayName: Run Completions Core lib tests using VS Code - script: xvfb-run -a npm run test:sanity displayName: Run extension sanity tests using VS Code tsa: config: areaPath: 'Visual Studio Code Copilot Extensions' serviceTreeID: '1788a767-5861-45fb-973b-c686b67c5541' enabled: true ${{ if eq(parameters.customNPMRegistry, false) }}: customNPMRegistry: '' generateNotice: ${{ parameters.generateNotice }} publishExtension: ${{ parameters.publishExtension }} ghReleasePublishVSIX: true ================================================ FILE: build/setup-emsdk.sh ================================================ mkdir -p /opt/dev \ && cd /opt/dev \ && git clone https://github.com/emscripten-core/emsdk.git \ && cd /opt/dev/emsdk \ && git reset --hard 0fde04880048f743056bed17cb0543a42e040fae \ && ./emsdk install 3.1.55 \ && ./emsdk activate 3.1.55 ================================================ FILE: build/update-assets.yml ================================================ trigger: none pr: none resources: repositories: - repository: 1esPipelines type: git name: 1ESPipelineTemplates/1ESPipelineTemplates ref: refs/tags/release extends: template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines parameters: sdl: tsa: enabled: false git: submodules: false fetchTags: false pool: name: 1es-windows-2022-x64 os: windows stages: - stage: PrepareAssetsDeploy displayName: "Prepare Assets for Deployment" jobs: - job: PrepareAssetsDeploy timeoutInMinutes: 30 templateContext: outputs: - output: pipelineArtifact path: "$(Build.SourcesDirectory)/assets/walkthroughs" artifact: walkthrough_assets steps: [] - stage: UploadWalkthroughAssets displayName: "Upload Walkthrough Assets" dependsOn: PrepareAssetsDeploy condition: succeeded() jobs: - job: UploadWalkthroughAssets timeoutInMinutes: 30 templateContext: type: releaseJob isProduction: true inputs: - input: pipelineArtifact artifactName: walkthrough_assets targetPath: $(Build.ArtifactStagingDirectory)/walkthrough_assets steps: - task: AzureFileCopy@6 displayName: "Upload Walkthrough Videos to Azure Blob Storage" inputs: SourcePath: $(Build.ArtifactStagingDirectory)/walkthrough_assets azureSubscription: "vscode-cdn" Destination: "AzureBlob" storage: "vscodewalkthroughs" ContainerName: "$web" BlobPrefix: "walkthroughs" ================================================ FILE: cgmanifest.json ================================================ { "$schema": "https://json.schemastore.org/component-detection-manifest.json", "Registrations": [ { "Component": { "Type": "git", "git": { "Name": "codex", "RepositoryUrl": "https://github.com/openai/codex", "CommitHash": "acc4acc81eea0339ad46d1c6f8459f58eaee6211" }, "DevelopmentDependency": false } } ] } ================================================ FILE: chat-lib/.gitignore ================================================ # Generated source files /src/ /dist/ /*.tgz ================================================ FILE: chat-lib/LICENSE.txt ================================================ MIT License Copyright (c) Microsoft Corporation. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: chat-lib/README.md ================================================ # @vscode/chat-lib Chat SDK extracted from VS Code Copilot Chat. ## Installation ```bash npm install @vscode/chat-lib ``` ## License MIT ================================================ FILE: chat-lib/package.json ================================================ { "name": "@vscode/chat-lib", "version": "0.0.0", "description": "Chat and inline editing SDK extracted from VS Code Copilot Chat", "main": "dist/src/main.js", "types": "dist/src/main.d.ts", "author": "Microsoft Corporation", "repository": { "type": "git", "url": "https://github.com/microsoft/vscode-copilot-chat.git" }, "license": "SEE LICENSE IN LICENSE.txt", "engines": { "node": ">=22.14.0" }, "dependencies": { "@microsoft/tiktokenizer": "^1.0.10", "@sinclair/typebox": "^0.34.41", "@vscode/copilot-api": "^0.2.15", "@vscode/l10n": "^0.0.18", "@vscode/prompt-tsx": "^0.4.0-alpha.6", "@vscode/tree-sitter-wasm": "0.0.5-php.2", "applicationinsights": "^2.9.7", "jsonc-parser": "^3.3.1", "monaco-editor": "0.44.0", "openai": "^6.7.0", "undici": "^7.18.2", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.12", "web-tree-sitter": "^0.23.0", "yaml": "^2.8.0" }, "devDependencies": { "@anthropic-ai/sdk": "^0.78.0", "@octokit/types": "^14.1.0", "@types/node": "^22.16.3", "@types/vscode": "^1.108.1", "copyfiles": "^2.4.1", "dotenv": "^17.2.0", "npm-run-all": "^4.1.5", "outdent": "^0.8.0", "rimraf": "^6.0.1", "tsx": "^4.20.3", "typescript": "^5.8.3", "vitest": "^3.0.5" }, "keywords": [ "chat", "ai", "sdk", "vscode", "copilot" ], "files": [ "dist/src", "README.md", "LICENSE.txt", "!**/test/**", "dist/src/_internal/util/common/test/shims", "script" ], "scripts": { "clean": "rimraf dist", "build": "npm-run-all compile copy", "compile": "tsc", "copy": "copyfiles src/package.json \"src/**/*.tiktoken\" dist", "package": "npm pack", "postinstall": "tsx script/postinstall.js", "publish": "npm publish", "test": "vitest run", "test:watch": "vitest" } } ================================================ FILE: chat-lib/script/postinstall.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; import * as path from 'path'; async function copyStaticAssets(srcpaths: string[], dst: string): Promise { await Promise.all(srcpaths.map(async srcpath => { const src = path.join(REPO_ROOT, srcpath); const dest = path.join(REPO_ROOT, dst, path.basename(srcpath)); await fs.promises.mkdir(path.dirname(dest), { recursive: true }); await fs.promises.copyFile(src, dest); })); } async function fileExists(filePath: string): Promise { try { await fs.promises.access(filePath, fs.constants.F_OK); return true; } catch { return false; } } const treeSitterGrammars: string[] = [ 'tree-sitter-c-sharp', 'tree-sitter-cpp', 'tree-sitter-go', 'tree-sitter-javascript', // Also includes jsx support 'tree-sitter-python', 'tree-sitter-ruby', 'tree-sitter-typescript', 'tree-sitter-tsx', 'tree-sitter-java', 'tree-sitter-rust', 'tree-sitter-php' ]; const REPO_ROOT = path.join(__dirname, '..'); async function platformDir(): Promise { const distPath = 'dist/src/_internal/platform'; const srcPath = 'src/_internal/platform'; if (await fileExists(path.join(REPO_ROOT, distPath))) { return distPath; } else if (await fileExists(path.join(REPO_ROOT, srcPath))) { return srcPath; } else { throw new Error('Could not find the source directory for tokenizer files'); } } function treeSitterWasmDir(): string { const modulePath = path.dirname(require.resolve('@vscode/tree-sitter-wasm')); return path.relative(REPO_ROOT, modulePath); } async function main() { const platform = await platformDir(); const vendoredTiktokenFiles = [`${platform}/tokenizer/node/cl100k_base.tiktoken`, `${platform}/tokenizer/node/o200k_base.tiktoken`]; const wasm = treeSitterWasmDir(); // copy static assets to dist await copyStaticAssets([ ...vendoredTiktokenFiles, ...treeSitterGrammars.map(grammar => `${wasm}/${grammar}.wasm`), `${wasm}/tree-sitter.wasm`, ], 'dist'); } main(); ================================================ FILE: chat-lib/tsconfig.base.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es2022", "lib": ["ES2022"], "sourceMap": true, "experimentalDecorators": true, "noImplicitOverride": true, "noUnusedLocals": true, "useDefineForClassFields": false, "allowUnreachableCode": false, "strict": true, "exactOptionalPropertyTypes": false, "useUnknownInCatchVariables": false, "noFallthroughCasesInSwitch": true, "forceConsistentCasingInFileNames": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, "skipLibCheck": true } } ================================================ FILE: chat-lib/tsconfig.json ================================================ { "extends": "./tsconfig.base.json", "compilerOptions": { "jsx": "react", "jsxFactory": "vscpp", "jsxFragmentFactory": "vscppf", "rootDir": ".", "outDir": "dist", "declaration": true, "declarationMap": true, "types": [ "node" ], "paths": {} }, "include": [ "src", "test" ], "exclude": [] } ================================================ FILE: chat-lib/vitest.config.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { loadEnv } from 'vite'; import { defineConfig } from 'vitest/config'; export default defineConfig(({ mode }) => ({ test: { include: ['**/*.spec.ts', '**/*.spec.tsx'], exclude: [ '**/node_modules/**', '**/dist/**', '**/.{idea,git,cache,output,temp}/**' ], env: loadEnv(mode, process.cwd(), ''), environment: 'node', globals: true } })); ================================================ FILE: docs/NES_EXPECTED_EDIT_CAPTURE.md ================================================ # NES Expected Edit Capture Feature ## Overview A feature that allows users to record/capture their "expected suggestion" when a Next Edit Suggestion (NES) was rejected or failed to appear. The captured data is saved in `.recording.w.json` format (compatible with stest infrastructure) for analysis and model improvement. ## Getting Started ### 1. Enable the Feature Add this setting to your VS Code `settings.json`: ```json { // Enable the capture feature "github.copilot.chat.advanced.inlineEdits.recordExpectedEdit.enabled": true } ``` That's it! Auto-capture on rejection is enabled by default. To disable it (you can still capture manually via **Cmd+K Cmd+R**): ```json { "github.copilot.chat.advanced.inlineEdits.recordExpectedEdit.onReject": false } ``` ### 2. Capture an Expected Edit **When NES shows a wrong suggestion:** 1. Reject the suggestion (press `Esc` or continue typing) 2. If `onReject` is enabled, capture mode starts automatically 3. Type the code you *expected* NES to suggest 4. Press **Enter** to save, or **Esc** to cancel **When NES didn't appear but should have:** 1. Press **Cmd+K Cmd+R** (Mac) or **Ctrl+K Ctrl+R** (Windows/Linux) 2. Type the code you expected NES to suggest 3. Press **Enter** to save > **Tip:** Use **Shift+Enter** to insert newlines during capture (since Enter saves). ### 3. Submit Your Feedback Once you've captured some edits: 1. Open Command Palette (**Cmd+Shift+P** / **Ctrl+Shift+P**) 2. Run **"Copilot: Submit NES Captures"** 3. Review the files to be included (you can exclude sensitive files) 4. Click **Submit Feedback** to create a PR ### Quick Reference | Action | Keybinding | |--------|------------| | Start capture manually | **Cmd+K Cmd+R** / **Ctrl+K Ctrl+R** | | Save capture | **Enter** | | Cancel capture | **Esc** | | Insert newline | **Shift+Enter** | | Command | Description | |---------|-------------| | Copilot: Record Expected Edit (NES) | Start a capture session | | Copilot: Submit NES Captures | Upload feedback to internal repo | ## How It Works ### Trigger Points - **Automatic**: Capture starts when you reject an NES suggestion (if `onReject` setting is enabled) - **Manual**: Use the keyboard shortcut or Command Palette when NES didn't appear but should have ### Capture Session When capture mode is active: 1. A status bar indicator shows: **"NES CAPTURE MODE ACTIVE"** 2. Type your expected edit naturally in the editor 3. Press **Enter** to save or **Esc** to cancel ### Where Captures Are Saved Recordings are stored in your workspace under `.copilot/nes-feedback/`: - `capture-.recording.w.json` — The edit recording - `capture-.metadata.json` — Context about the capture --- ## Technical Reference ### Commands | Command ID | Description | |------------|-------------| | `github.copilot.nes.captureExpected.start` | Start capture manually | | `github.copilot.nes.captureExpected.confirm` | Confirm and save | | `github.copilot.nes.captureExpected.abort` | Cancel capture | | `github.copilot.nes.captureExpected.submit` | Submit to `microsoft/copilot-nes-feedback` | ### Architecture #### State Management The capture controller maintains minimal state: ```typescript { active: boolean; startBookmark: DebugRecorderBookmark; endBookmark?: DebugRecorderBookmark; startDocumentId: DocumentId; startTime: number; trigger: 'rejection' | 'manual'; originalNesMetadata?: { requestUuid: string; providerInfo?: string; modelName?: string; endpointUrl?: string; suggestionText?: string; // [startLine, startCharacter, endLine, endCharacter] suggestionRange?: [number, number, number, number]; documentPath?: string; }; } ``` ### Implementation Flow The capture flow leverages **DebugRecorder**, which already tracks all document edits automatically—no custom event listeners or manual diff computation needed. 1. **Start Capture**: Create a bookmark in DebugRecorder, store the current document ID, set context key `copilotNesCaptureMode` to enable keybindings, and show status bar indicator. 2. **User Edits**: User types their expected edit naturally in the editor. DebugRecorder automatically tracks all changes in the background. 3. **Confirm Capture**: Create an end bookmark, extract the log slice between start/end bookmarks, filter for edits on the target document, compose them into a single `nextUserEdit`, and save to disk. 4. **Abort/Cleanup**: Clear state, reset context key, and dispose status bar item. See `ExpectedEditCaptureController` in [vscode-node/components/expectedEditCaptureController.ts](vscode-node/components/expectedEditCaptureController.ts) for the full implementation. ### File Output #### Location Recordings are stored in the **first workspace folder** under the `.copilot/nes-feedback/` directory: - **Full path**: `/.copilot/nes-feedback/capture-.recording.w.json` - **Timestamp format**: ISO 8601 with colons/periods replaced by hyphens (e.g., `2025-12-04T14-30-45`) - **Example**: `.copilot/nes-feedback/capture-2025-12-04T14-30-45.recording.w.json` - The folder is automatically created if it doesn't exist Each recording generates two files: 1. **Recording file**: `capture-.recording.w.json` - Contains the log and edit data 2. **Metadata file**: `capture-.metadata.json` - Contains capture context and timing #### Format Matches existing `.recording.w.json` structure used by stest infrastructure: ```json { "log": [ { "kind": "header", "repoRootUri": "file:///workspace", "time": 1234567890, "uuid": "..." }, { "kind": "documentEncountered", "id": 0, "relativePath": "src/foo.ts", "time": 1234567890 }, { "kind": "setContent", "id": 0, "v": 1, "content": "...", "time": 1234567890 }, ... ], "nextUserEdit": { "relativePath": "src/foo.ts", "edit": [ [876, 996, "replaced text"], [1522, 1530, "more text"] ] } } ``` #### Metadata File A metadata file is saved alongside each recording with capture context: ```jsonc { "captureTimestamp": "2025-11-19T...", // ISO timestamp when capture started "trigger": "rejection", // How capture was initiated: 'rejection' or 'manual' "durationMs": 5432, // Time between start and confirm in milliseconds "noEditExpected": false, // True if user confirmed without making edits "originalNesContext": { // Metadata from the rejected NES suggestion (if any) "requestUuid": "...", // Unique ID of the NES request "providerInfo": "...", // Source of the suggestion (e.g., 'provider', 'diagnostics') "modelName": "...", // AI model that generated the suggestion "endpointUrl": "...", // API endpoint used for the request "suggestionText": "...", // The actual suggested text that was rejected "suggestionRange": [10, 0, 15, 20] // [startLine, startChar, endLine, endChar] of suggestion } } ``` ## Benefits - **Zero-friction**: Type naturally, press Enter — no forms or dialogs - **Works for both**: Rejected suggestions and missed opportunities - **Privacy-aware**: Sensitive files are automatically filtered before submission ## Edge Cases | Scenario | Behavior | |----------|----------| | **Multiple rapid rejections** | Only one capture active at a time; subsequent rejections ignored | | **Document closed** | Capture automatically aborted | | **No edits made** | Valid feedback! Saved with `noEditExpected: true` (indicates the rejection was correct) | | **Large edits** | DebugRecorder handles size limits automatically | ## Feedback Submission When you run **"Copilot: Submit NES Captures"**: 1. All captures from `.copilot/nes-feedback/` are collected 2. A preview dialog shows which files will be included 3. You can exclude specific files if needed 4. A pull request is created in `microsoft/copilot-nes-feedback` ### Privacy & Filtering Sensitive files are **automatically excluded** from submissions: - VS Code settings (`settings.json`, `launch.json`) - Credentials (`.npmrc`, `.env`, `.gitconfig`, etc.) - Private keys (`.pem`, `.key`, `id_rsa`, etc.) - Sensitive directories (`.aws/`, `.ssh/`, `.gnupg/`) **Requirements:** GitHub authentication with repo access to `microsoft/copilot-nes-feedback` --- ## Future Enhancements - **Diff Preview**: Show visual comparison before saving - **Category Tagging**: Quick-pick to categorize expectation type (import, refactor, etc.) - **Auto-Generate stest**: Create `.stest.ts` wrapper file automatically ## Related Files - [node/debugRecorder.ts](node/debugRecorder.ts) - Core recording infrastructure - [vscode-node/components/inlineEditDebugComponent.ts](vscode-node/components/inlineEditDebugComponent.ts) - Existing feedback/debug tooling and sensitive file filtering - [vscode-node/components/expectedEditCaptureController.ts](vscode-node/components/expectedEditCaptureController.ts) - Capture session management - [vscode-node/components/nesFeedbackSubmitter.ts](vscode-node/components/nesFeedbackSubmitter.ts) - Feedback submission to GitHub - [common/observableWorkspaceRecordingReplayer.ts](common/observableWorkspaceRecordingReplayer.ts) - Recording replay logic - [../../../test/simulation/inlineEdit/inlineEditTester.ts](../../../test/simulation/inlineEdit/inlineEditTester.ts) - stest infrastructure ================================================ FILE: docs/monitoring/agent_monitoring.md ================================================ # Monitoring Agent Usage with OpenTelemetry Copilot Chat can export **traces**, **metrics**, and **events** via [OpenTelemetry](https://opentelemetry.io/) (OTel) — giving you real-time visibility into agent interactions, LLM calls, tool executions, and token usage. All signal names and attributes follow the [OTel GenAI Semantic Conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/), so the data works with any OTel-compatible backend: Jaeger, Grafana, Azure Monitor, Datadog, Honeycomb, and more. ## Quick Start The fastest way to see Copilot Chat traces locally — no cloud account required. This guide uses the [Aspire Dashboard](https://aspire.dev/dashboard/standalone/), a lightweight container image from Microsoft that provides a trace viewer with a built-in OTLP endpoint. It can be used standalone, without the rest of .NET Aspire. ### Prerequisites - **Docker** installed - **VS Code** with the GitHub Copilot Chat extension ### 1. Start the Aspire Dashboard ```bash docker run --rm -d \ -p 18888:18888 \ -p 4317:18889 \ --name aspire-dashboard \ mcr.microsoft.com/dotnet/aspire-dashboard:latest ``` This exposes the dashboard UI on port `18888` and an OTLP (gRPC) endpoint on port `4317`. ### 2. Configure VS Code Open **Settings** (`Ctrl+,`) and add: ```json { "github.copilot.chat.otel.enabled": true, "github.copilot.chat.otel.exporterType": "otlp-grpc", "github.copilot.chat.otel.otlpEndpoint": "http://localhost:4317" } ``` > **Note:** You can also use environment variables instead of VS Code settings (see [Configuration](#configuration)). Environment variables always take precedence. ### 3. Generate Telemetry Open Copilot Chat and send any message — for example, ask a question in Agent mode. ### 4. View Traces Open http://localhost:18888 → **Traces**. You'll see `invoke_agent` spans with nested `chat` and `execute_tool` children. ### Teardown ```bash docker stop aspire-dashboard ``` > **Tip:** See the [Aspire Dashboard standalone docs](https://aspire.dev/dashboard/standalone/) for more configuration options. --- ## Configuration ### VS Code Settings Open **Settings** (`Ctrl+,`) and search for `copilot otel`: | Setting | Type | Default | Description | |---|---|---|---| | `github.copilot.chat.otel.enabled` | boolean | `false` | Enable OTel emission | | `github.copilot.chat.otel.exporterType` | string | `"otlp-http"` | `otlp-http`, `otlp-grpc`, `console`, or `file` | | `github.copilot.chat.otel.otlpEndpoint` | string | `"http://localhost:4318"` | OTLP collector endpoint | | `github.copilot.chat.otel.captureContent` | boolean | `false` | Capture full prompt/response content | | `github.copilot.chat.otel.outfile` | string | `""` | File path for JSON-lines output | ### Environment Variables Environment variables **always take precedence** over VS Code settings. | Variable | Default | Description | |---|---|---| | `COPILOT_OTEL_ENABLED` | `false` | Enable OTel. Also enabled when `OTEL_EXPORTER_OTLP_ENDPOINT` is set. | | `COPILOT_OTEL_ENDPOINT` | — | OTLP endpoint URL (takes precedence over `OTEL_EXPORTER_OTLP_ENDPOINT`) | | `OTEL_EXPORTER_OTLP_ENDPOINT` | — | Standard OTel OTLP endpoint URL | | `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/protobuf` | OTLP protocol. Only `grpc` changes behavior; all other values use HTTP. | | `COPILOT_OTEL_PROTOCOL` | — | Override OTLP protocol (`grpc` or `http`). Falls back to `OTEL_EXPORTER_OTLP_PROTOCOL`. | | `OTEL_SERVICE_NAME` | `copilot-chat` | Service name in resource attributes | | `OTEL_RESOURCE_ATTRIBUTES` | — | Extra resource attributes (`key1=val1,key2=val2`) | | `COPILOT_OTEL_CAPTURE_CONTENT` | `false` | Capture full prompt/response content | | `COPILOT_OTEL_LOG_LEVEL` | `info` | Min log level: `trace`, `debug`, `info`, `warn`, `error` | | `COPILOT_OTEL_FILE_EXPORTER_PATH` | — | Write all signals to this file (JSON-lines) | | `COPILOT_OTEL_HTTP_INSTRUMENTATION` | `false` | Enable HTTP-level OTel instrumentation | | `OTEL_EXPORTER_OTLP_HEADERS` | — | Auth headers (e.g., `Authorization=Bearer token`) | ### Activation OTel is **off by default** with zero overhead. It activates when: - `COPILOT_OTEL_ENABLED=true`, or - `OTEL_EXPORTER_OTLP_ENDPOINT` is set, or - `github.copilot.chat.otel.enabled` is `true` --- ## What Gets Exported ### Traces Copilot Chat emits a hierarchical span tree for each agent interaction: ``` invoke_agent copilot [~15s] ├── chat gpt-4o [~3s] (LLM requests tool calls) ├── execute_tool readFile [~50ms] ├── execute_tool runCommand [~2s] ├── chat gpt-4o [~4s] (LLM generates final response) └── (span ends) ``` **`invoke_agent`** — wraps the entire agent orchestration (all LLM calls + tool executions). | Attribute | Requirement | Example | |---|---|---| | `gen_ai.operation.name` | Required | `invoke_agent` | | `gen_ai.provider.name` | Required | `github` | | `gen_ai.agent.name` | Required | `copilot` | | `gen_ai.conversation.id` | Required | `a1b2c3d4-...` | | `gen_ai.request.model` | Recommended | `gpt-4o` | | `gen_ai.response.model` | Recommended | `gpt-4o-2024-08-06` | | `gen_ai.usage.input_tokens` | Recommended | `12500` | | `gen_ai.usage.output_tokens` | Recommended | `3200` | | `copilot_chat.turn_count` | Always | `4` | | `error.type` | On error | `Error` | | `gen_ai.input.messages` | Opt-in (captureContent) | `[{"role":"user",...}]` | | `gen_ai.output.messages` | Opt-in (captureContent) | `[{"role":"assistant",...}]` | | `gen_ai.tool.definitions` | Opt-in (captureContent) | `[{"type":"function",...}]` | **`chat`** — one span per LLM API call (span kind: `CLIENT`). | Attribute | Requirement | Example | |---|---|---| | `gen_ai.operation.name` | Required | `chat` | | `gen_ai.provider.name` | Required | `github` | | `gen_ai.request.model` | Required | `gpt-4o` | | `gen_ai.conversation.id` | Required | `a1b2c3d4-...` | | `gen_ai.request.max_tokens` | Always | `2048` | | `gen_ai.request.temperature` | When set | `0.1` | | `gen_ai.request.top_p` | When set | `0.95` | | `copilot_chat.request.max_prompt_tokens` | Always | `128000` | | `gen_ai.response.id` | On response | `chatcmpl-abc123` | | `gen_ai.response.model` | On response | `gpt-4o-2024-08-06` | | `gen_ai.response.finish_reasons` | On response | `["stop"]` | | `gen_ai.usage.input_tokens` | On response | `1500` | | `gen_ai.usage.output_tokens` | On response | `250` | | `copilot_chat.time_to_first_token` | On response | `450` | | `server.address` | When available | `api.github.com` | | `copilot_chat.debug_name` | When available | `agentMode` | | `error.type` | On error | `TimeoutError` | | `gen_ai.input.messages` | Opt-in (captureContent) | `[{"role":"system",...}]` | | `gen_ai.system_instructions` | Opt-in (captureContent) | `[{"type":"text",...}]` | **`execute_tool`** — one span per tool invocation (span kind: `INTERNAL`). | Attribute | Requirement | Example | |---|---|---| | `gen_ai.operation.name` | Required | `execute_tool` | | `gen_ai.tool.name` | Required | `readFile` | | `gen_ai.tool.type` | Required | `function` or `extension` (MCP tools) | | `gen_ai.tool.call.id` | Recommended | `call_abc123` | | `gen_ai.tool.description` | When available | `Read the contents of a file` | | `error.type` | On error | `FileNotFoundError` | | `gen_ai.tool.call.arguments` | Opt-in (captureContent) | `{"filePath":"/src/index.ts"}` | | `gen_ai.tool.call.result` | Opt-in (captureContent) | `(file contents or summary)` | ### Metrics #### GenAI Convention Metrics | Metric | Type | Unit | Description | |---|---|---|---| | `gen_ai.client.operation.duration` | Histogram | s | LLM API call duration | | `gen_ai.client.token.usage` | Histogram | tokens | Token counts (input/output) | **`gen_ai.client.operation.duration` attributes:** | Attribute | Description | |---|---| | `gen_ai.operation.name` | Operation type (e.g., `chat`) | | `gen_ai.provider.name` | Provider (e.g., `github`, `anthropic`) | | `gen_ai.request.model` | Requested model | | `gen_ai.response.model` | Resolved model (if different) | | `server.address` | Server hostname | | `server.port` | Server port | | `error.type` | Error class (if failed) | **`gen_ai.client.token.usage` attributes:** | Attribute | Description | |---|---| | `gen_ai.operation.name` | Operation type | | `gen_ai.provider.name` | Provider name | | `gen_ai.token.type` | `input` or `output` | | `gen_ai.request.model` | Requested model | | `gen_ai.response.model` | Resolved model | | `server.address` | Server hostname | #### Extension-Specific Metrics | Metric | Type | Unit | Description | |---|---|---|---| | `copilot_chat.tool.call.count` | Counter | calls | Tool invocations by name and success | | `copilot_chat.tool.call.duration` | Histogram | ms | Tool execution latency | | `copilot_chat.agent.invocation.duration` | Histogram | s | Agent mode end-to-end duration | | `copilot_chat.agent.turn.count` | Histogram | turns | LLM round-trips per agent invocation | | `copilot_chat.session.count` | Counter | sessions | Chat sessions started | | `copilot_chat.time_to_first_token` | Histogram | s | Time to first SSE token | **`copilot_chat.tool.call.count` attributes:** `gen_ai.tool.name`, `success` (boolean) **`copilot_chat.tool.call.duration` attributes:** `gen_ai.tool.name` **`copilot_chat.agent.invocation.duration` attributes:** `gen_ai.agent.name` **`copilot_chat.agent.turn.count` attributes:** `gen_ai.agent.name` **`copilot_chat.time_to_first_token` attributes:** `gen_ai.request.model` ### Events #### `gen_ai.client.inference.operation.details` Emitted after each LLM API call with full inference metadata. | Attribute | Description | |---|---| | `gen_ai.operation.name` | Always `chat` | | `gen_ai.request.model` | Requested model | | `gen_ai.response.model` | Resolved model | | `gen_ai.response.id` | Response ID | | `gen_ai.response.finish_reasons` | Stop reasons (e.g., `["stop"]`) | | `gen_ai.usage.input_tokens` | Input token count | | `gen_ai.usage.output_tokens` | Output token count | | `gen_ai.request.temperature` | Temperature (if set) | | `gen_ai.request.max_tokens` | Max tokens (if set) | | `error.type` | Error class (if failed) | | `gen_ai.input.messages` | Full prompt messages (captureContent only) | | `gen_ai.system_instructions` | System prompt (captureContent only) | | `gen_ai.tool.definitions` | Tool schemas (captureContent only) | #### `copilot_chat.session.start` Emitted when a new chat session begins (top-level agent invocations only, not subagents). | Attribute | Description | |---|---| | `session.id` | Session identifier | | `gen_ai.request.model` | Initial model | | `gen_ai.agent.name` | Chat participant name | #### `copilot_chat.tool.call` Emitted when a tool invocation completes. | Attribute | Description | |---|---| | `gen_ai.tool.name` | Tool name | | `duration_ms` | Execution time in milliseconds | | `success` | `true` or `false` | | `error.type` | Error class (if failed) | #### `copilot_chat.agent.turn` Emitted for each LLM round-trip within an agent invocation. | Attribute | Description | |---|---| | `turn.index` | Turn number (0-indexed) | | `gen_ai.usage.input_tokens` | Input tokens this turn | | `gen_ai.usage.output_tokens` | Output tokens this turn | | `tool_call_count` | Number of tool calls this turn | ### Resource Attributes All signals carry: | Attribute | Value | |---|---| | `service.name` | `copilot-chat` (configurable via `OTEL_SERVICE_NAME`) | | `service.version` | Extension version | | `session.id` | Unique per VS Code window | Add custom resource attributes with `OTEL_RESOURCE_ATTRIBUTES`: ```bash export OTEL_RESOURCE_ATTRIBUTES="team.id=platform,department=engineering" ``` These custom attributes are included in all traces, metrics, and events, allowing you to: - Filter metrics by team or department - Create team-specific dashboards and alerts - Track usage across organizational boundaries > **Note:** `OTEL_RESOURCE_ATTRIBUTES` uses comma-separated `key=value` pairs. Values cannot contain spaces, commas, or semicolons. Use percent-encoding for special characters (e.g., `org.name=John%27s%20Org`). --- ## Content Capture By default, **no prompt content, responses, or tool arguments are captured** — only metadata like model names, token counts, and durations. To capture full content, add to your VS Code settings: ```json { "github.copilot.chat.otel.captureContent": true } ``` This populates these span attributes: | Attribute | Content | |---|---| | `gen_ai.input.messages` | Full prompt messages (JSON) | | `gen_ai.output.messages` | Full response messages (JSON) | | `gen_ai.system_instructions` | System prompt | | `gen_ai.tool.definitions` | Tool schemas | | `gen_ai.tool.call.arguments` | Tool input arguments | | `gen_ai.tool.call.result` | Tool output | Content is captured in full with no truncation. > **Warning:** Content capture may include sensitive information such as code, file contents, and user prompts. Only enable in trusted environments. --- ## Example Configurations **OTLP/gRPC:** ```json { "github.copilot.chat.otel.enabled": true, "github.copilot.chat.otel.exporterType": "otlp-grpc", "github.copilot.chat.otel.otlpEndpoint": "http://localhost:4317" } ``` **Remote collector with authentication:** ```json { "github.copilot.chat.otel.enabled": true, "github.copilot.chat.otel.otlpEndpoint": "https://collector.example.com:4318" } ``` > **Note:** Authentication headers are only configurable via the `OTEL_EXPORTER_OTLP_HEADERS` environment variable (e.g., `Authorization=Bearer your-token`). See [Environment Variables](#environment-variables). **File-based output (offline / CI):** ```json { "github.copilot.chat.otel.enabled": true, "github.copilot.chat.otel.exporterType": "file", "github.copilot.chat.otel.outfile": "/tmp/copilot-otel.jsonl" } ``` **Console output (quick debugging):** ```json { "github.copilot.chat.otel.enabled": true, "github.copilot.chat.otel.exporterType": "console" } ``` --- ## Subagent Trace Propagation When an agent invokes a subagent (e.g., via the `runSubagent` tool), Copilot Chat automatically propagates the trace context so the subagent's `invoke_agent` span is parented to the calling agent's `execute_tool` span. This produces a connected trace tree: ``` invoke_agent copilot [~30s] ├── chat gpt-4o [~3s] ├── execute_tool runSubagent [~20s] │ └── invoke_agent Explore [~18s] ← child via trace context │ ├── chat gpt-4o [~2s] │ ├── execute_tool searchFiles [~200ms] │ ├── execute_tool readFile [~50ms] │ └── chat gpt-4o [~3s] ├── chat gpt-4o [~4s] └── (span ends) ``` This propagation works across async boundaries — the parent's trace context is stored when `runSubagent` starts and retrieved when the subagent begins its `invoke_agent` span. --- ## Background Agents (Copilot CLI) When OTel is enabled, **all agent types** are automatically instrumented — no additional configuration needed. The same settings that enable foreground agent traces also enable Copilot CLI traces. ### Copilot CLI (Background Agent) The Copilot CLI SDK runs in the same VS Code process and produces a rich trace hierarchy including subagents, permissions, hooks, and tool calls: ``` copilot-chat invoke_agent copilotcli [~45s] ← extension wrapper └── github-copilot invoke_agent [~42s] ← SDK native spans ├── chat claude-sonnet-4.6 [~16s] │ ├── hook postToolUse ← hook execution │ └── hook postToolUse ├── execute_tool task [~18s] │ └── invoke_agent task ← subagent │ ├── chat claude-sonnet-4.6 │ ├── execute_tool bash │ │ └── permission │ └── execute_tool report_intent ├── chat claude-sonnet-4.6 [~4s] └── hook sessionEnd ← session lifecycle hook ``` The extension wrapper span (`invoke_agent copilotcli`, service `copilot-chat`) parents the SDK's native spans (service `github-copilot`). Both appear in the same trace in your collector. **Agent Debug Log panel**: CLI sessions show the full SDK hierarchy in the Tree View — identical to what appears in Grafana/Jaeger. This works even when OTel export is disabled, because the SDK's internal tracing is always active for the debug panel. ### Copilot CLI (Terminal Session) Terminal CLI sessions ("New Copilot CLI Session") run as a separate process. When OTel is enabled, the extension forwards `COPILOT_OTEL_ENABLED` and `OTEL_EXPORTER_OTLP_ENDPOINT` to the terminal process. Terminal traces appear as **independent root traces** (service `github-copilot`) — they are not linked to extension traces. > **Note:** The CLI runtime only supports `otlp-http`. When `otlp-grpc` is configured, the terminal CLI still uses HTTP. Backends that serve both protocols on the same port (e.g., Aspire Dashboard) work transparently. ### Filtering by Agent Type In your trace viewer, filter by `service.name` to see traces from specific agents: | `service.name` | Source | |---|---| | `copilot-chat` | Foreground agent + CLI wrapper spans | | `github-copilot` | CLI SDK native spans + CLI terminal | --- ## Interpreting the Data **Traces** — Visualize the full agent execution in Jaeger or Grafana Tempo. Each `invoke_agent` span contains child `chat` and `execute_tool` spans, making it easy to identify bottlenecks and debug failures. Subagent invocations appear as nested `invoke_agent` spans under `execute_tool runSubagent`. **Metrics** — Track token usage trends by model and provider, monitor tool success rates via `copilot_chat.tool.call.count`, and watch perceived latency with `copilot_chat.time_to_first_token`. All metrics carry the same resource attributes (`service.name`, `service.version`, `session.id`) for consistent filtering. **Events** — `copilot_chat.session.start` tracks session creation. `copilot_chat.tool.call` events provide per-invocation timing and error details. `gen_ai.client.inference.operation.details` gives the full LLM call record including token usage and, when content capture is enabled, the complete prompt/response messages. Use `gen_ai.conversation.id` to correlate all signals belonging to the same session. --- ## Initialization & Buffering The OTel SDK is loaded asynchronously via dynamic imports to avoid blocking extension startup. Events emitted before initialization completes are buffered (up to 1,000 items) and replayed once the SDK is ready. If initialization fails, buffered events are discarded and all subsequent calls become no-ops — the extension continues to function normally. First successful span export is logged to the console (`[OTel] First span batch exported successfully via ...`) to confirm end-to-end connectivity. --- ## Backend Setup Guides Copilot Chat's OTel data works with any OTLP-compatible backend. This section provides step-by-step setup guides for recommended backends. ### Aspire Dashboard See [Quick Start](#quick-start) above for setup. The [Aspire Dashboard](https://aspire.dev/dashboard/standalone/) is the simplest option — a single Docker container with a built-in OTLP endpoint and trace viewer. No cloud account or collector needed. ### OTel Collector + Azure Application Insights [Azure Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview) ingests OTel traces, metrics, and logs through an [OTel Collector](https://opentelemetry.io/docs/collector/) with the `azuremonitor` exporter. This repo includes a ready-to-use collector setup in `docs/monitoring/`. **1. Create an Application Insights resource:** 1. Go to the [Azure Portal](https://portal.azure.com/). 2. Click **Create a resource** → search **Application Insights** → **Create**. 3. Choose your subscription, resource group, name, and region → **Review + Create** → **Create**. 4. Once deployed, go to the resource → **Overview** → copy the **Connection String**. **2. Start the OTel Collector:** ```bash export APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=...;IngestionEndpoint=..." cd docs/monitoring docker compose up -d ``` Verify the collector is healthy: ```bash # Should return 200 curl -s -o /dev/null -w "%{http_code}" http://localhost:4328/v1/traces \ -X POST -H "Content-Type: application/json" -d '{"resourceSpans":[]}' ``` **3. Configure VS Code:** Open **Settings** (`Ctrl+,`) and add: ```json { "github.copilot.chat.otel.enabled": true, "github.copilot.chat.otel.exporterType": "otlp-http", "github.copilot.chat.otel.otlpEndpoint": "http://localhost:4328" } ``` Optionally, to capture full prompt/response content: ```json { "github.copilot.chat.otel.captureContent": true } ``` > **Warning:** Content capture includes prompts, code, and file contents. Only enable in trusted environments. **4. Generate telemetry** — Open Copilot Chat and send any message (e.g., use Agent mode). **5. Verify data:** - **Jaeger (local):** Open http://localhost:16687, select service `copilot-chat`, click **Find Traces**. - **App Insights (Azure):** Go to your Application Insights resource → **Transaction search** → filter by "Trace" or "Request". Run this query in **Application Insights → Logs** to confirm: ```kql traces | where timestamp > ago(1h) | where message contains "GenAI" or message contains "copilot_chat" | project timestamp, message, customDimensions | order by timestamp desc ``` For metrics (may take 5–10 minutes to appear): ```kql customMetrics | where timestamp > ago(1h) | where name startswith "gen_ai" or name startswith "copilot_chat" | summarize avg(value), count() by name ``` **Collector config** (`docs/monitoring/otel-collector-config.yaml`): ```yaml receivers: otlp: protocols: http: endpoint: 0.0.0.0:4318 grpc: endpoint: 0.0.0.0:4317 exporters: azuremonitor: connection_string: "${APPLICATIONINSIGHTS_CONNECTION_STRING}" debug: verbosity: basic service: pipelines: traces: receivers: [otlp] exporters: [azuremonitor, debug] metrics: receivers: [otlp] exporters: [azuremonitor, debug] ``` > **Note:** The docker-compose maps ports to `4328`/`4327` on the host to avoid conflicts. Adjust in `docker-compose.yaml` if needed. Add additional exporters (e.g., `otlphttp/jaeger`) to fan out to multiple backends. See `docs/monitoring/otel-collector-config.yaml` for the full config including `batch` processor and `logs` pipeline. ### Jaeger [Jaeger](https://www.jaegertracing.io/) is an open-source distributed tracing platform. It accepts OTLP directly — no collector needed. **1. Start Jaeger:** ```bash docker run -d --name jaeger -p 16686:16686 -p 4318:4318 jaegertracing/jaeger:latest ``` **2. Configure VS Code:** ```json { "github.copilot.chat.otel.enabled": true, "github.copilot.chat.otel.otlpEndpoint": "http://localhost:4318" } ``` **3. Verify:** Open http://localhost:16686, select service `copilot-chat`, and click **Find Traces**. ### Langfuse [Langfuse](https://langfuse.com/) is an open-source LLM observability platform with native OTLP ingestion and support for [OTel GenAI Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-agent-spans/). See the [Langfuse docs](https://langfuse.com/docs/opentelemetry/introduction) for full details on capabilities and limitations. **Setup:** ```json { "github.copilot.chat.otel.enabled": true, "github.copilot.chat.otel.otlpEndpoint": "http://localhost:3000/api/public/otel", "github.copilot.chat.otel.captureContent": true } ``` Then set the auth header via environment variable (required — no VS Code setting for headers): ```bash export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic $(echo -n ':' | base64)" ``` Replace `` and `` with your Langfuse API keys from **Settings → API Keys**. **Verify:** Open Langfuse → **Traces**. You should see `invoke_agent` traces with nested `chat` and `execute_tool` spans. ### Other Backends Any OTLP-compatible backend works with Copilot Chat's OTel output. Some options: | Backend | Description | |---|---| | **[Jaeger](https://www.jaegertracing.io/)** | Open-source distributed tracing platform | | **[Grafana Tempo](https://grafana.com/oss/tempo/) + [Prometheus](https://prometheus.io/)** | Open-source traces + metrics stack | Refer to each backend's documentation for OTLP ingestion setup. --- ## Security & Privacy - **Off by default.** No OTel data is emitted unless explicitly enabled. When disabled, the OTel SDK is not loaded at all — zero runtime overhead. - **No content by default.** Prompts, responses, and tool arguments require opt-in via `captureContent`. - **No PII in default attributes.** Session IDs, model names, and token counts are not personally identifiable. - **User-configured endpoints.** Data goes only where you point it — no phone-home behavior. - **Dynamic imports only.** OTel SDK packages are loaded on-demand, ensuring zero bundle impact when disabled. ================================================ FILE: docs/monitoring/agent_monitoring_arch.md ================================================ # OTel Instrumentation — Developer Guide This document describes the architecture, code structure, and conventions for the OpenTelemetry instrumentation in the Copilot Chat extension. It covers all four agent execution paths. For user-facing configuration and usage, see [agent_monitoring.md](agent_monitoring.md). For a visual data flow diagram, see [otel-data-flow.html](otel-data-flow.html). --- ## Multi-Agent Architecture The extension has four agent execution paths, each with different OTel strategies: | Agent | Process Model | Strategy | Debug Panel Source | |---|---|---|---| | **Foreground** (toolCallingLoop) | Extension host | Direct `IOTelService` spans | Extension spans | | **Copilot CLI in-process** | Extension host (same process) | **Bridge SpanProcessor** — SDK creates spans natively; bridge forwards to debug panel | SDK native spans via bridge | | **Copilot CLI terminal** | Separate terminal process | Forward OTel env vars | N/A (separate process) | | **Claude Code** | Child process (Node fork) | **Synthetic spans** — extension creates spans from message loop | Extension synthetic spans | > **Why asymmetric?** The CLI SDK runs in-process with full trace hierarchy (subagents, permissions, hooks). A bridge captures this directly. Claude runs as a separate process — internal spans are inaccessible, so synthetic spans are the only option. ### Copilot CLI Bridge SpanProcessor The extension injects a `CopilotCliBridgeSpanProcessor` into the SDK's `BasicTracerProvider` to forward completed spans to the debug panel. See [otel-data-flow.html](otel-data-flow.html) for the full visual diagram. ``` Extension Root (tracer A): invoke_agent copilotcli → traceparent → SDK SDK Native (tracer B, same traceId): invoke_agent → chat → execute_tool → invoke_agent (subagent) → permission → ... Bridge: SDK Provider B → MultiSpanProcessor._spanProcessors.push(bridge) → onEnd(ReadableSpan) → ICompletedSpanData + CHAT_SESSION_ID → IOTelService.injectCompletedSpan → onDidCompleteSpan → Debug Panel + File Logger ``` **⚠️ SDK Internal Access Warning**: The bridge accesses `_delegate._activeSpanProcessor._spanProcessors` — internal properties of the OTel SDK v2 `BasicTracerProvider`. This is necessary because v2 removed the public `addSpanProcessor()` API. The SDK itself uses this same pattern in `forceFlush()`. This may break on OTel SDK major version upgrades — the bridge includes a runtime guard that degrades gracefully. ### Span Hierarchies #### Foreground Agent ``` invoke_agent copilot (INTERNAL) ← toolCallingLoop.ts ├── chat gpt-4o (CLIENT) ← chatMLFetcher.ts │ ├── execute_tool readFile (INTERNAL) ← toolsService.ts │ └── execute_tool runCommand (INTERNAL) ├── chat gpt-4o (CLIENT) └── ... ``` #### Copilot CLI in-process (Bridge) ``` invoke_agent copilotcli (INTERNAL) ← copilotcliSession.ts (tracer A) └── [traceparent linked] invoke_agent (CLIENT) ← SDK OtelSessionTracker (tracer B) ├── chat claude-opus-4.6-1m (CLIENT) ├── execute_tool task (INTERNAL) │ └── invoke_agent task (CLIENT) ← SUBAGENT │ ├── chat claude-opus-4.6-1m │ ├── execute_tool bash │ │ └── permission │ └── execute_tool report_intent ├── chat claude-opus-4.6-1m (CLIENT) └── ... ``` #### Copilot CLI terminal (independent) ``` invoke_agent (CLIENT) ← standalone copilot binary │ service.name = github-copilot ├── chat gpt-4o (CLIENT) └── (independent root traces, no extension link) ``` #### Claude Code (synthetic) ``` invoke_agent claude (INTERNAL) ← claudeCodeAgent.ts ├── chat claude-sonnet-4 (CLIENT) ← chatMLFetcher.ts (FREE) ├── execute_hook PreToolUse (INTERNAL) ← claudeHookRegistry.ts (PR #4578) ├── execute_tool Read (INTERNAL) ← message loop (PR #4505) ├── execute_hook PostToolUse (INTERNAL) ← claudeHookRegistry.ts (PR #4578) ├── chat claude-sonnet-4 (CLIENT) ├── execute_hook PreToolUse (INTERNAL) ├── execute_tool Edit (INTERNAL) ├── execute_hook PostToolUse (INTERNAL) └── (flat hierarchy — no subagent nesting) ``` --- ## File Structure ``` src/platform/otel/ ├── common/ │ ├── otelService.ts # IOTelService interface + ISpanHandle + injectCompletedSpan │ ├── otelConfig.ts # Config resolution (env → settings → defaults) │ ├── noopOtelService.ts # Zero-cost no-op implementation │ ├── agentOTelEnv.ts # deriveCopilotCliOTelEnv / deriveClaudeOTelEnv │ ├── genAiAttributes.ts # GenAI semantic convention attribute keys │ ├── genAiEvents.ts # Event emitter helpers │ ├── genAiMetrics.ts # GenAiMetrics class (metric recording) │ ├── messageFormatters.ts # Message → OTel JSON schema converters │ ├── index.ts # Public API barrel export │ └── test/ └── node/ ├── otelServiceImpl.ts # NodeOTelService (real SDK implementation) ├── inMemoryOTelService.ts # InMemoryOTelService (debug panel, no SDK) ├── fileExporters.ts # File-based span/log/metric exporters └── test/ src/extension/chatSessions/copilotcli/node/ ├── copilotCliBridgeSpanProcessor.ts # Bridge: SDK spans → IOTelService ├── copilotcliSession.ts # Root invoke_agent span + traceparent └── copilotcliSessionService.ts # Bridge installation + env var setup src/extension/trajectory/vscode-node/ ├── otelChatDebugLogProvider.ts # Debug panel data provider └── otelSpanToChatDebugEvent.ts # Span → ChatDebugEvent conversion ``` ### Instrumentation Points | File | What Gets Instrumented | |---|---| | `chatMLFetcher.ts` | `chat` spans — all LLM API calls (foreground + Claude proxy) | | `anthropicProvider.ts` | `chat` spans — BYOK Anthropic requests | | `toolCallingLoop.ts` | `invoke_agent` spans — foreground agent orchestration | | `toolsService.ts` | `execute_tool` spans — foreground tool invocations | | `copilotcliSession.ts` | `invoke_agent copilotcli` wrapper span + traceparent propagation | | `copilotCliBridgeSpanProcessor.ts` | Bridge: SDK `ReadableSpan` → `ICompletedSpanData` | | `copilotcliSessionService.ts` | Bridge installation + OTel env vars for SDK | | `copilotCLITerminalIntegration.ts` | OTel env vars forwarded to terminal process | | `claudeCodeAgent.ts` | `invoke_agent claude` + `execute_tool` synthetic spans | | `claudeHookRegistry.ts` | `execute_hook` spans — Claude hook executions (PR #4578) | | `otelSpanToChatDebugEvent.ts` | Span → debug panel event conversion | --- ## Service Layer ### `IOTelService` Interface The core abstraction. Consumers depend on this interface, never on the OTel SDK directly. Key methods: - `startSpan` / `startActiveSpan` — create trace spans - `injectCompletedSpan` — inject externally-created spans (bridge uses this) - `onDidCompleteSpan` — event fired when any span ends (debug panel listens) - `recordMetric` / `incrementCounter` — metrics - `emitLogRecord` — OTel log events - `storeTraceContext` / `runWithTraceContext` — cross-boundary propagation ### Implementations | Class | When Used | |---|---| | `NoopOTelService` | OTel disabled (default) — zero cost | | `NodeOTelService` | OTel enabled — full SDK, OTLP export | | `InMemoryOTelService` | Debug panel always-on — no SDK, in-memory only | ### Two TracerProviders in Same Process When the CLI SDK is active with OTel enabled: - **Provider A** (`NodeOTelService`): Extension's provider, stored tracer ref survives global override - **Provider B** (`BasicTracerProvider`): SDK's provider, replaces A as global Both export to the same OTLP endpoint. Bridge processor sits on Provider B, forwards to Provider A's event emitter. --- ## Configuration `resolveOTelConfig()` implements layered precedence: 1. `COPILOT_OTEL_*` env vars (highest) 2. `OTEL_EXPORTER_OTLP_*` standard env vars 3. VS Code settings (`github.copilot.chat.otel.*`) 4. Defaults (lowest) Kill switch: `telemetry.telemetryLevel === 'off'` → all OTel disabled. ### Agent-Specific Env Var Translation | Extension Config | Copilot CLI Env Var | Claude Code Env Var | |---|---|---| | `enabled` | `COPILOT_OTEL_ENABLED=true` | `CLAUDE_CODE_ENABLE_TELEMETRY=1` | | `otlpEndpoint` | `OTEL_EXPORTER_OTLP_ENDPOINT` | `OTEL_EXPORTER_OTLP_ENDPOINT` | | `captureContent` | `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true` | `OTEL_LOG_USER_PROMPTS=1` | | `fileExporterPath` | `COPILOT_OTEL_FILE_EXPORTER_PATH` | N/A | ### Debug Panel Always-On Behavior The CLI SDK's OTel (`OtelLifecycle`) is **always initialized** regardless of user OTel settings. This ensures the debug panel always receives SDK native spans via the bridge. The `COPILOT_OTEL_ENABLED` env var is set before `LocalSessionManager` construction so the SDK creates its `OtelSessionTracker`. When user OTel is **disabled**: SDK spans flow through bridge → debug panel only (no OTLP export). When user OTel is **enabled**: SDK spans flow through bridge → debug panel AND through SDK's own `BatchSpanProcessor` → OTLP. ### `service.name` Values | Source | `service.name` | |---|---| | Extension (Provider A) | `copilot-chat` | | Copilot CLI SDK / terminal | `github-copilot` | | Claude Code subprocess | `claude-code` | --- ## Span Conventions Follow the OTel GenAI semantic conventions. Use constants from `genAiAttributes.ts`: | Operation | Span Name | Kind | |---|---|---| | Agent orchestration | `invoke_agent {agent_name}` | `INTERNAL` | | LLM API call | `chat {model}` | `CLIENT` | | Tool execution | `execute_tool {tool_name}` | `INTERNAL` | | Hook execution | `execute_hook {hook_type}` | `INTERNAL` | ### Debug Panel Display Names The debug panel uses span names directly for display (matching Grafana): - Tool calls: `execute_tool {tool_name}` (from `span.name`) - Hook executions: `execute_hook {hook_type}` (from `span.name`) - Subagent invocations: `invoke_agent {agent_name}` (from `span.name`) - SDK wrapper `invoke_agent` spans without an agent name are skipped as transparent containers ### Error Handling ```typescript span.setStatus(SpanStatusCode.ERROR, error.message); span.setAttribute(StdAttr.ERROR_TYPE, error.constructor.name); ``` ### Content Capture Gate on `otel.config.captureContent`: ```typescript if (this._otelService.config.captureContent) { span.setAttribute(GenAiAttr.INPUT_MESSAGES, JSON.stringify(messages)); } ``` --- ## Adding Instrumentation ### Pattern: Wrapping an Operation ```typescript return this._otel.startActiveSpan( 'execute_tool myTool', { kind: SpanKind.INTERNAL, attributes: { [GenAiAttr.TOOL_NAME]: 'myTool' } }, async (span) => { try { const result = await this._actualWork(); span.setStatus(SpanStatusCode.OK); return result; } catch (err) { span.setStatus(SpanStatusCode.ERROR, err instanceof Error ? err.message : String(err)); throw err; } }, ); ``` ### Pattern: Cross-Boundary Trace Propagation ```typescript // Parent: store context const ctx = this._otelService.getActiveTraceContext(); if (ctx) { this._otelService.storeTraceContext(`subagent:${id}`, ctx); } // Child: retrieve and use as parent const parentCtx = this._otelService.getStoredTraceContext(`subagent:${id}`); return this._otel.startActiveSpan('invoke_agent child', { parentTraceContext: parentCtx }, ...); ``` --- ## Attribute Namespaces | Namespace | Used By | Examples | |---|---|---| | `gen_ai.*` | All agents (standard) | `gen_ai.operation.name`, `gen_ai.usage.input_tokens` | | `copilot_chat.*` | Extension-specific | `copilot_chat.session_id`, `copilot_chat.chat_session_id` | | `github.copilot.*` | CLI SDK internal | `github.copilot.cost`, `github.copilot.aiu` | | `claude_code.*` | Claude subprocess | `claude_code.token.usage`, `claude_code.cost.usage` | --- ## Debug Panel vs OTLP Isolation The debug panel creates spans with non-standard operation names (`content_event`, `user_message`). These MUST NOT appear in the user's OTLP collector. `DiagnosticSpanExporter` in `NodeOTelService` filters spans: only `invoke_agent`, `chat`, `execute_tool`, `embeddings`, `execute_hook` are exported. The `execute_hook` operation is used by both the foreground agent (`toolCallingLoop.ts`) and Claude hooks (`claudeHookRegistry.ts`, PR #4578). Debug-panel-only spans are visible via `onDidCompleteSpan` but excluded from OTLP batch export. --- ## Testing ``` src/platform/otel/common/test/ ├── agentOTelEnv.spec.ts # Env var derivation ├── genAiEvents.spec.ts ├── genAiMetrics.spec.ts ├── messageFormatters.spec.ts ├── noopOtelService.spec.ts └── otelConfig.spec.ts src/platform/otel/node/test/ ├── fileExporters.spec.ts └── traceContextPropagation.spec.ts src/extension/chatSessions/copilotcli/node/test/ └── copilotCliBridgeSpanProcessor.spec.ts # Bridge processor tests ``` Run with: `npm test -- --grep "OTel\|Bridge"` --- ## Risks & Known Limitations | Risk | Impact | Mitigation | |---|---|---| | SDK `_spanProcessors` internal access | May break on OTel SDK v2 minor/major updates | Runtime guard with graceful fallback; same pattern SDK uses in `forceFlush()` | | Two TracerProviders in same process | Span context may not cross provider boundary | Extension stores tracer ref; traceparent propagated explicitly | | `process.env` mutation for CLI SDK | Affects extension host globally | Only set OTel-specific vars; set before SDK ctor | | Duplicate `invoke_agent` spans in OTLP | Extension root + SDK root both exported | Different `service.name` distinguishes them | | Claude file exporter not supported | Claude subprocess can't write to JSON-lines file | Documented limitation | | CLI runtime only supports `otlp-http` | Terminal CLI can't use gRPC-only endpoints | Documented limitation | ================================================ FILE: docs/monitoring/docker-compose.yaml ================================================ # Copilot Chat OTel monitoring stack # # Starts an OpenTelemetry Collector that accepts OTLP on :4318 (HTTP) and :4317 (gRPC), # then forwards traces/metrics/logs to Azure Application Insights and a local Jaeger instance. # # Usage: # # Set your App Insights connection string: # export APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=...;IngestionEndpoint=..." # # # Start the stack: # docker compose up -d # # # View traces in Jaeger: # open http://localhost:16687 # # # Then launch VS Code with: # COPILOT_OTEL_ENABLED=true OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4328 code . services: otel-collector: image: otel/opentelemetry-collector-contrib:latest command: ["--config=/etc/otel-collector-config.yaml"] volumes: - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml:ro ports: - "4327:4317" # OTLP gRPC (host:4327 → container:4317) - "4328:4318" # OTLP HTTP (host:4328 → container:4318) environment: - APPLICATIONINSIGHTS_CONNECTION_STRING=${APPLICATIONINSIGHTS_CONNECTION_STRING:-} restart: unless-stopped jaeger: image: jaegertracing/jaeger:latest ports: - "16687:16686" # Jaeger UI (host:16687 to avoid conflict) restart: unless-stopped ================================================ FILE: docs/monitoring/otel-collector-config.yaml ================================================ # OpenTelemetry Collector configuration for Copilot Chat # Receives OTLP from Copilot Chat and exports to multiple backends. # # Usage: # docker compose -f docs/monitoring/docker-compose.yaml up -d # # Then set in VS Code or launch.json: # OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 receivers: otlp: protocols: http: endpoint: 0.0.0.0:4318 grpc: endpoint: 0.0.0.0:4317 processors: batch: timeout: 5s send_batch_size: 256 exporters: # Azure Application Insights via connection string # Replace with your App Insights connection string azuremonitor: connection_string: "${APPLICATIONINSIGHTS_CONNECTION_STRING}" # Debug exporter — prints to collector stdout (useful for troubleshooting) debug: verbosity: basic # Local Jaeger for trace visualization otlphttp/jaeger: endpoint: http://jaeger:4318 service: pipelines: traces: receivers: [otlp] processors: [batch] exporters: [azuremonitor, otlphttp/jaeger, debug] metrics: receivers: [otlp] processors: [batch] exporters: [azuremonitor, debug] logs: receivers: [otlp] processors: [batch] exporters: [azuremonitor, debug] ================================================ FILE: docs/monitoring/otel-data-flow.html ================================================ OTel Data Flow — All Agents

OTel Data Flow — VS Code Copilot Chat Extension

Complete tracing architecture for foreground agent, CLI background agent, CLI terminal, and Claude Code

Provider A (Extension NodeOTelService)
Provider B (SDK BasicTracerProvider)
Bridge SpanProcessor
Debug Panel Consumer
External Exporter
Synthetic Spans (ext-created)
Overview (All Agents)
Foreground Agent
CLI Background
CLI Terminal
Claude Code
VS Code Extension Host Process Provider A: NodeOTelService Registered at extension activation tracer A (stored ref, survives override) service.name = "copilot-chat" Emitters: onDidCompleteSpan, onDidEmitSpanEvent Provider B: SDK BasicTracerProvider Created by OtelLifecycle.initializeOtelSdk() Replaces Provider A as global service.name = "github-copilot" SpanProcessors: BatchSP + BridgeSP CopilotCliBridgeSpanProcessor Added to Provider B after trackSession() Maps traceId → sessionId, injects CHAT_SESSION_ID Filters: only forwards registered traceIds Foreground Agent toolCallingLoop.ts → chatMLFetcher.ts Uses tracer A (Provider A) invoke_agent → chat → execute_tool CLI Background Agent OtelSessionTracker (SDK native) Uses tracer B (Provider B / global) invoke_agent → chat → execute_tool + subagents, permissions, hooks CLI Extension Wrapper copilotcliSession.ts invoke_agent copilotcli (tracer A) Claude Code Agent claudeCodeAgent.ts (synthetic spans) Uses tracer A (Provider A) invoke_agent → execute_tool (flat) Claude Code Subprocess Separate Node process Metrics + events only (no traces) CLI Terminal Process Separate terminal process Full SDK OTel (independent traces) OTelChatDebugLogProvider Listens: onDidCompleteSpan Buckets by copilot_chat.chat_session_id Feeds: Agent Debug Log panel UI ChatDebugFileLoggerService Listens: onDidCompleteSpan Writes: debug-logs//main.jsonl OTLP Exporter (Provider A) BatchSpanProcessor → HTTP/gRPC endpoint Exports foreground + CLI wrapper spans OTLP Exporter (Provider B) BatchSpanProcessor → same HTTP endpoint Exports SDK native spans Grafana / Jaeger / Collector Receives all spans from all exporters Shows full trace hierarchy onDidCompleteSpan BatchSpanProcessor injectCompletedSpan() onEnd() traceparent env vars independent traces metrics+events synthetic spans resolveOTelConfig() Settings: github.copilot.chat.otel.* Env: COPILOT_OTEL_*, OTEL_EXPORTER_* Kill switch: telemetry.telemetryLevel=off OTelConfig
Foreground Agent — Provider A Only All spans created by tracer A (extension's NodeOTelService) Span Hierarchy (tracer A) invoke_agent copilot (INTERNAL) — toolCallingLoop.ts chat gpt-4o (CLIENT) — chatMLFetcher.ts execute_tool readFile (INTERNAL) — toolsService.ts execute_tool runCommand (INTERNAL) — toolsService.ts chat gpt-4o (CLIENT) — chatMLFetcher.ts execute_tool runSubagent (INTERNAL) — toolsService.ts invoke_agent Explore (INTERNAL) — toolCallingLoop.ts All spans have copilot_chat.chat_session_id NodeOTelService Provider A tracer onDidCompleteSpan Debug Panel ✅ OTLP → Grafana ✅ ✅ Fully working Same spans in debug panel and Grafana
CLI Background Agent — Two Providers (Bridge Architecture) Extension root span (tracer A) + SDK native spans (tracer B) + Bridge for debug panel Extension Root (tracer A) invoke_agent copilotcli — copilotcliSession.ts CHAT_SESSION_ID ✅ | CONVERSATION_ID ✅ | SESSION_ID ✅ ↓ traceparent = 00-{traceId}-{spanId}-01 ↓ via otelLifecycle.updateParentTraceContext() SDK spans inherit this traceId → same trace in Grafana ✅ SDK Native Spans (tracer B, same traceId) invoke_agent (CLIENT) — OtelSessionTracker chat claude-opus-4.6-1m (CLIENT) execute_tool report_intent execute_tool task invoke_agent explore (SUBAGENT!) execute_tool grep + permission chat claude-opus-4.6-1m (final response) Bridge injects copilot_chat.chat_session_id on each span ✅ All spans reach debug panel via bridge Provider B (SDK) BatchSpanProcessor → OTLP BridgeSpanProcessor → ext (injected) BridgeSpanProcessor ✅ Injected via internal _spanProcessors ProxyTP._delegate._activeSpanProcessor._spanProcessors.push() NodeOTelService (Provider A) injectCompletedSpan → fire event Debug Panel ✅ (full hierarchy) onDidCompleteSpan OTLP → Grafana ✅ HOW: Bridge injection via OTel v2 internals 1. api.trace.getTracerProvider() → ProxyTracerProvider 2. ._delegate → BasicTracerProvider 3. ._activeSpanProcessor → MultiSpanProcessor 4. ._spanProcessors.push(bridge) — same pattern SDK uses in forceFlush() ✅ Full data flow working • Grafana: full hierarchy via Provider B → BatchSpanProcessor → OTLP • Debug Panel: full hierarchy via Provider B → Bridge → injectCompletedSpan → onDidCompleteSpan • Trace context: SDK spans are children of extension root span (traceparent propagation)
CLI Terminal — Separate Process, Independent Traces Extension only forwards env vars. No bridge. No debug panel visibility. Extension (at terminal creation) deriveCopilotCliOTelEnv(config, {}) → TerminalOptions.env env vars Terminal Process (copilot binary) Own OtelLifecycle + BasicTracerProvider service.name = "github-copilot" invoke_agent → chat → execute_tool Independent root traceId No CHAT_SESSION_ID (not needed) Grafana ✅ Debug Panel ❌ Separate process — spans never reach extension
Claude Code — Synthetic Spans (Only Option) Child process internal spans inaccessible. Extension creates spans from message loop. Extension Synthetic Spans (tracer A) invoke_agent claude — claudeCodeAgent.ts chat claude-sonnet-4 — chatMLFetcher.ts (FREE) execute_tool Read — message loop (PR #4505) chat claude-sonnet-4 — chatMLFetcher.ts execute_tool Edit — message loop (PR #4505) FLAT hierarchy — no subagent nesting, no permissions Claude Code Subprocess (separate process) Own OTel SDK: metrics + events (no traces) claude_code.token.usage, claude_code.cost.usage claude_code.tool_result, claude_code.api_request NodeOTelService Provider A tracer Debug Panel ✅ (flat) OTLP → Grafana ✅ subprocess metrics/events WHY SYNTHETIC (not bridge)? • Claude SDK runs as separate child process • Internal spans are inaccessible from extension • Bridge approach is impossible — synthetic is the only option

Key Design Facts

Two TracerProviders in the Same Process

  • Provider A (NodeOTelService): Created at extension activation. Stores tracer ref. Serves foreground agent, CLI wrapper span, Claude synthetic spans.
  • Provider B (BasicTracerProvider): Created by SDK's OtelLifecycle.initializeOtelSdk(). Replaces A as global. Serves all SDK-native spans.
  • Provider A's stored this._tracer still works after override — OTel tracers are bound to their provider at creation time.

Bridge SpanProcessor

  • Injected into Provider B via internal _spanProcessors.push()
  • Converts ReadableSpanICompletedSpanData
  • Injects copilot_chat.chat_session_id from traceId → sessionId map
  • Filters: only forwards spans whose traceId is registered (CLI sessions)
  • Fires IOTelService.injectCompletedSpan()onDidCompleteSpan

Trace Context Propagation

  • Extension root span → traceparent = 00-{traceId}-{spanId}-01
  • Passed to SDK via otelLifecycle.updateParentTraceContext(sessionId, traceparent)
  • SDK spans inherit the extension's traceId → same trace in Grafana
  • Bridge matches traceId → injects CHAT_SESSION_ID → debug panel buckets correctly

Debug Panel Data Path

  • Foreground: tracer A → TrackedSpanHandle.end() → _onDidCompleteSpan.fire()
  • CLI background: tracer B → Bridge.onEnd() → injectCompletedSpan()_onDidCompleteSpan.fire()
  • Claude: tracer A → TrackedSpanHandle.end() → _onDidCompleteSpan.fire() (same as foreground)
  • CLI terminal: N/A (separate process, no debug panel visibility)
  • Both consumers: OTelChatDebugLogProvider (trajectory) and ChatDebugFileLoggerService (file)

service.name Values

  • copilot-chat — Extension Provider A spans (foreground, CLI wrapper, Claude synthetic)
  • github-copilot — SDK Provider B spans (CLI native), CLI terminal
  • claude-code — Claude subprocess metrics/events

What Goes Where

  • Grafana: Everything from all OTLP exporters (Provider A + B + terminal + Claude subprocess)
  • Debug Panel: Only spans that fire onDidCompleteSpan with copilot_chat.chat_session_id
  • File Logger: Same as debug panel (listens to same event)
================================================ FILE: docs/prompts.md ================================================ # Authoring Model-Specific Prompts ## Table of Contents - [Overview](#overview) - [Creating a Custom Prompt](#creating-a-custom-prompt) **Development Workflow** - [Step 1: Start with Defaults](#step-1-start-with-defaults) - [Step 2: Test Behaviors](#step-2-test-behaviors) - [Step 3: Make Minimal Adjustments](#step-3-make-minimal-adjustments) **Validation** - [Expected Behaviors](#expected-behaviors) - [Common Pitfalls](#common-pitfalls) - [Testing & Debugging](#testing--debugging) ## Overview vscode-copilot-chat uses a **prompt registry system** to map AI models to their optimal prompt structures. Each model provider can have customized prompts that leverage provider-specific strengths. ### How the Registry Works The [`PromptRegistry`](../src/extension/prompts/node/agent/promptRegistry.ts) matches models to prompts using: 1. **Custom matchers**: `matchesModel()` functions for complex logic 2. **Family prefixes**: Simple string matching on model family names A single prompt resolver can return **different prompts for different models** within the same provider family. For example, you might want to use one prompt for `gpt-5` and a different optimized prompt for `gpt-5-codex`. The resolver's `resolvePrompt()` method receives the endpoint information (including the model name) and can use conditional logic to return the appropriate prompt class: ```typescript class MyProviderPromptResolver implements IAgentPrompt { static readonly familyPrefixes = ['my-model']; resolvePrompt(endpoint: IChatEndpoint): PromptConstructor | undefined { // Different prompts for different model versions if (endpoint.model?.startsWith('my-model-1')) { return MyModel1Prompt; // Optimized for 1 variant } if (endpoint.model?.startsWith('my-model-4')) { return MyModel4Prompt; // Optimized for standard v4 } return MyDefaultPrompt; // Fallback for other models } } ``` This allows fine-grained control over prompts while keeping all model variants organized in a single provider file. ### File Locations Prompts are located in `src/extension/prompts/node/agent/`: - **[defaultAgentInstructions.tsx](../src/extension/prompts/node/agent/defaultAgentInstructions.tsx)** - Base prompt and shared components - **[promptRegistry.ts](../src/extension/prompts/node/agent/promptRegistry.ts)** - **[anthropicPrompts.tsx](../src/extension/prompts/node/agent/anthropicPrompts.tsx)** - **[openAIPrompts.tsx](../src/extension/prompts/node/agent/openAIPrompts.tsx)** - **[geminiPrompts.tsx](../src/extension/prompts/node/agent/geminiPrompts.tsx)** - **[xAIPrompts.tsx](../src/extension/prompts/node/agent/xAIPrompts.tsx)** - **[vscModelPrompts.tsx](../src/extension/prompts/node/agent/vscModelPrompts.tsx)** --- ## Creating a Custom Prompt ### Step 1: Copy Default Prompt Structure Copy `DefaultAgentPrompt` from `defaultAgentInstructions.tsx` into your custom prompt file: ```tsx import { PromptElement, PromptSizing } from '@vscode/prompt-tsx'; import { DefaultAgentPromptProps, detectToolCapabilities } from './defaultAgentInstructions'; import { InstructionMessage } from '../base/instructionMessage'; export class MyProviderAgentPrompt extends PromptElement { async render(state: void, sizing: PromptSizing) { const tools = detectToolCapabilities(this.props.availableTools); return {/* Your customizations here */} ; } } ``` ### Step 2: Create Resolver Implement `IAgentPrompt` interface: ```typescript class MyProviderPromptResolver implements IAgentPrompt { constructor( @IConfigurationService private readonly configurationService: IConfigurationService, @IExperimentationService private readonly experimentationService: IExperimentationService, ) { } static readonly familyPrefixes = ['my-model', 'provider-name']; resolvePrompt(endpoint: IChatEndpoint): PromptConstructor | undefined { // Simple: One prompt for all return MyProviderAgentPrompt; // Advanced: Conditional logic if (endpoint.model?.startsWith('my-model-4')) { return MyProviderV4Prompt; } return MyProviderAgentPrompt; } } ``` ### Step 3: Register Register your prompt at the end of the file: ```typescript PromptRegistry.registerPrompt(MyProviderPromptResolver); ``` Finally, update the [`allAgentPrompts.ts`](../src/extension/prompts/node/agent/allAgentPrompts.ts) file to include your custom prompt file. --- ## Step 1: Start with Defaults Begin with `DefaultAgentPrompt` - no customizations. Most models infer correct behavior from tool definitions alone. The default prompt consists of minimal instructions: - Basic role definition: "You are a highly sophisticated automated coding agent..." - Tool availability awareness: Conditional instructions based on available tools - Response formatting: Markdown rules, file/symbol linkification When optimizing prompts, follow these principles: - Start simple and add only when needed - Remove redundancy to improve token efficiency - Use conditional sections to include only relevant instructions - Maintain consistent terminology that matches tool names --- ## Step 2: Test Behaviors Run the model through test scenarios and evaluate key behaviors: ### 1. Tool Usage Patterns - Uses edit tools (`replace_string_in_file`, `apply_patch`, `insert_edit_into_file`) instead of code blocks - Uses code search tools (`read_file`, `semantic_search`, `grep_search`, `file_search`) to gather context - Uses terminal tool (`run_in_terminal`) instead of bash commands - **Does NOT use terminal tools to create, edit, or update files** - always uses dedicated edit tools instead - Uses planning tools (`manage_todo_list`) for complex tasks ### 2. Response Format - File paths and symbols linkified (wrapped in backticks) - Structured markdown with headers and sections - Concise, well-timed progress updates between tool calls **Common issue**: Some models front-load thinking and only summarize at the end. Sample fix: ```tsx Provide brief progress updates every 3-5 tool calls to keep the user informed of your progress.
After completing parallel tool calls, provide a brief status update before proceeding to the next step.
``` ### 3. Workflow Execution - Gathers context before acting - Completes tasks end-to-end without pausing to check with user - Handles errors and iterates appropriately --- ## Step 3: Make Minimal Adjustments **Only customize if the model consistently fails Step 2 behaviors.** Add 1-2 sentences targeting the specific issue: ```tsx // Fix: Model shows code blocks instead of using edit tools {tools[ToolName.ReplaceStringInFile] && <> NEVER print out a code block with file changes unless the user asked for it. Use the appropriate edit tool (replace_string_in_file, apply_patch, or insert_into_file). } // Fix: Model calls terminal tool in parallel {tools[ToolName.CoreRunInTerminal] && <> Don't call the run_in_terminal tool multiple times in parallel. Instead, run one command and wait for the output before running the next command. } // Fix: Model doesn't use TODO tool for planning {tools[ToolName.CoreManageTodoList] && <> For complex multi-step tasks, use the manage_todo_list tool to track your progress and provide visibility to the user. } ``` This approach keeps instructions targeted and avoids over-specification. ## Step 4: Testing Add your model family to the list at the top of [`agentPrompt.spec.tsx`](../src/extension/prompts/node/agent/test/agentPrompt.spec.tsx). That will render the prompt for your model with some different input scenarios and validate the prompt output against snapshots. This is really useful for checking the final rendered form of your prompt, ensuring any model-specific customizations have been applied correctly, and avoiding regressions. You can add test cases to cover any new dynamic logic in your prompt as needed. --- ## Expected Behaviors Key behaviors the model should exhibit: ### File/Symbol Linkification ✅ **Correct**: `` The function `calculateTotal` is in `lib/utils/math.ts` `` ❌ **Wrong**: `The function calculateTotal is in lib/utils/math.ts` ### Tool Usage for Code ✅ **Correct**: Calls edit tools (`replace_string_in_file`, `apply_patch`, `insert_into_file`) ❌ **Wrong**: Shows code in markdown blocks (unless explicitly requested) **Fix**: ```tsx NEVER print out a code block with file changes unless the user asked for it. Use the appropriate edit tool (replace_string_in_file, apply_patch, or insert_into_file). ``` ### Code Search Tools ✅ **Correct**: Uses `read_file`, `semantic_search`, `grep_search` to gather context before editing ❌ **Wrong**: Makes assumptions about code without reading it first, or uses terminal tools to read files **Fix**: ```tsx {tools[ToolName.ReadFile] && <> Always read relevant files using read_file before making changes. Use semantic_search to find related code across the workspace. } ``` ### Terminal Commands ✅ **Correct**: Calls `run_in_terminal` tool ❌ **Wrong**: Shows bash commands in code blocks (unless explicitly requested) **Fix**: ```tsx {tools[ToolName.CoreRunInTerminal] && <> NEVER print out a code block with a terminal command to run unless the user asked for it. Use the run_in_terminal tool instead. } ``` ### Response Format Best Practices **Short Preambles** ✅ `I'll add error handling to the login function.` ❌ `Thank you for your request! I understand you want me to add error handling. This is a great idea...` **Progress Updates** ✅ Brief updates every 3-5 tool calls ❌ Silent batch processing or constant narration **Completion Summaries** ✅ Well-structured markdown with headers, bullets, linkified files ❌ Unformatted walls of text **TODO Management** ✅ Creates, updates, and marks TODOs complete incrementally ❌ No task tracking for complex multi-step work --- ## Common Pitfalls ### Over-Specification ❌ **Bad**: Too many contradictory instructions ```tsx You should use tools. But also think first. Use tools when needed. Don't use tools unnecessarily. Use tools effectively. ``` ✅ **Good**: Clear and actionable ```tsx You can call tools repeatedly to gather context and complete tasks. Don't give up unless you're certain the request cannot be fulfilled. ``` ### Missing Tool Checks ❌ **Bad**: Assumes tool exists ```tsx Use the read_file tool to read files. ``` ✅ **Good**: Checks availability ```tsx {tools[ToolName.ReadFile] && <>Use the read_file tool to read files.} ``` ### Ignoring prompt-tsx Conventions ❌ **Bad**: Newlines ignored in JSX ```tsx Line 1 Line 2 ``` ✅ **Good**: Explicit breaks ```tsx Line 1
Line 2
``` --- ## Testing & Debugging Open the Chat Debug View to inspect the actual prompt, tool calls, and tool call results. This is the best way to check the exact prompt being sent to the model. ![](./media/debug-view.png) ![](./media/tool-log.png) ### Test Scenarios **Simple Queries** ``` User: what's the square root of 144? Expected: 12 ``` **File Operations** ``` User: Add error handling to auth.ts Expected: - Reads the file with read_file - Uses edit tool (replace_string_in_file, apply_patch, or insert_into_file, not code blocks) - Provides linkified references ``` **Multi-step Tasks** ``` User: Create a REST API for user management Expected: - Creates TODO list - Gathers context - Implements incrementally - Updates TODOs - Provides summary ``` **Code Search** ``` User: How does authentication work? Expected: - Uses code search tools (semantic_search, grep_search, read_file) - Structured markdown - Linkified references - Section headers ``` ================================================ FILE: docs/tools.md ================================================ ## So you want to write a tool New to LLM tools? Here are some starting resources - https://code.visualstudio.com/api/extension-guides/tools - https://docs.anthropic.com/en/docs/build-with-claude/tool-use/overview - https://platform.openai.com/docs/guides/function-calling?api-mode=chat - https://www.anthropic.com/engineering/building-effective-agents This is aimed at adding tools to vscode-copilot-chat, but much of it would apply to tools in other extensions or MCP servers as well. ### Do we need a new tool? First, consider whether a new built-in tool is needed. Tools should be built-in if they are related to core VS Code functionality or the core search/edit/terminal agent loop and are needed for common OOB scenarios. Consider whether the tool can be contributed from another extension instead. If the task can be done through normal terminal commands, then it may not need its own tool. ### Static part First, add an entry in vscode-copilot's package.json under `contributes.languageModelTools`: - ~~Give it a name that starts with `copilot_`- this pattern is protected for our use only~~ - This is obsolete- new tools can use any name, I think matching `toolReferenceName` might be a good idea. - The existing `copilot_` tools will be renamed later. - Give it a reasonable `toolReferenceName` and a localized `userDescription`. - `toolReferenceName` is the name used in the tool picker, and to reference the tool with `#`, and to add the tool to a mode or toolset. - Add it to a toolset in `contributes.languageModelToolSets`- new tools should be part of a toolset. - Now write your `modelDescription`. This is what the LLM uses to decide whether to use your tool. This should _not_ be localized. Be very detailed: - What exactly does the tool do? - What kind of information does it return? - In what cases should the tool be used? - Read more [best practices](https://docs.anthropic.com/en/docs/build-with-claude/tool-use/overview#best-practices-for-tool-definitions) - If the tool takes input, add an `inputSchema`. This is a JSON schema which must describe an object with the properties that the tool takes. Describe the properties in detail. File paths should be absolute paths. Think carefully about which properties are `required`. - In `toolNames.ts`, add entries to `ToolName`, `ContributedToolName`, `contributedToolNameToToolNames`. Follow the naming patterns of other tools. `ToolName` is the real name of your tool that the LLM will see. It should also be clear. A good pattern is to start with a verb, e.g. `read_file`. - And remember to look for other tools that do similar things, and try to ensure your tool is aligned with them in the input it takes and the terminology it uses, and doesn't overlap in behavior. That will ensure that an LLM can understand how to use them together. ### Tool implementation part Then, implement your tool in `src/extension/tools/node`: - If your tool takes input, write an interface and be sure that it matches the schema in package.json exactly, including which properties are required. - A typical tool can implement `vscode.LanguageModelTool`. More sophisticated tools can implement `ICopilotTool`, which gives you some extra functionality. - Call `ToolRegistry.registerTool(YourTool);` and import your tool file in `allTools.ts`. - Is your tool relevant in simulator/swebench scenarios? If so, check that it works. - I recommend using prompt-tsx for your tool result if it's not a simple string. This lets you compose the result from multiple parts or reuse other prompt-tsx components. ### Input validation - The input will be validated against the schema in package.json, so you don't need to repeat that validation in your tool. - When taking paths from the LLM as input, use `IPromptPathRepresentationService`. ### Error handling If something goes wrong, throw an error with a message that will make sense to the LLM. It will be caught by the agent and shown to the LLM. Should the model call your tool again with different arguments, or do something different? Make sure the model can understand what to do next. ### Tool confirmations If the tool has a potentially dangerous side-effect (e.g. the terminal tool), it MUST ask for the user's confirmation before running. Do this by returning `PreparedToolInvocation.confirmationMessages`. Give enough context in the confirmation message for the user to understand what the tool will do, and what the risk is. The `message` can be a markdown string containing a codeblock. ### Make it look good - Fill out `PreparedToolInvocation.invocationMessage` and `pastTenseMessage` with a helpful message to show in the UI. - Don't add your own `...` to the end of the tool message - If you want the tool message to react to the result of the tool, you can use `ExtendedLanguageModelToolResult.toolResultMessage`. - Use markdown where appropriate. - Setting `toolResultDetails` will make the tool message an expandable list of URIs to show the tool's result. (e.g. file search, text search) ![](./media/expandable-tool-result.png) - If you want a clickable file widget in the tool message (e.g. read file), set `ExtendedLanguageModelToolResult.toolResultMessage` to a MarkdownString, using `formatUriForFileWidget`. This currently can't be combined with the `toolResultDetails` option. ![](./media/file-widget.png) ### Testing Consider writing a unit test for your tool. One example to copy is [`readFile.spec.tsx`](https://github.com/microsoft/vscode-copilot/blob/a2b8af8b8e7286d4da77ff4108b6bcdeb1441d79/src/extension/tools/node/test/readFile.spec.tsx#L40-L59). This test invokes the tool with some hardcoded arguments and checks the result against a snapshot. ## Model-Specific Tools Model-specific tools allow you to provide tool implementations that are only available for certain language models (e.g., Gemini, Claude, GPT-5). This is useful when: - A model has unique capabilities that require a specialized tool - You want to adjust tool descriptions/schemas to work better with a specific model - You need to override an existing tool's behavior for certain models ### When to use model-specific tools Use model-specific tools when: - The tool leverages model-specific capabilities (e.g., native model features) - You need different tool descriptions or schemas that work better with certain models - You want to override behavior of an existing tool for specific models ### Registering a model-specific tool Register model-specific tools using `ToolRegistry.registerModelSpecificTool`: ```typescript class MyGeminiTool implements ICopilotModelSpecificTool { async invoke( options: vscode.LanguageModelToolInvocationOptions, token: vscode.CancellationToken ): Promise { // Gemini-specific implementation return { content: [{ type: 'text', value: 'Result' }] }; } } // Register with a model selector const disposable = ToolRegistry.registerModelSpecificTool( { name: 'my_gemini_tool', displayName: 'My Gemini Tool', description: 'A tool optimized for Gemini models', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'The query to process' } }, required: ['query'] }, // Only available for Gemini 3: models: [{ family: 'gemini-3-pro' }] }, MyGeminiTool ); ``` ### Overriding existing tools If your model-specific tool should **replace** an existing tool for certain models, use the `overridesTool` property: ```typescript class MyGeminiSearchTool extends GenericGrepSearchTool { public readonly overridesTool = ToolName.GrepSearch; // Gemini-optimized search implementation } } ToolRegistry.registerModelSpecificTool( { name: 'gemini_grep_search', displayName: 'Search (Gemini)', description: 'Optimized grep search for Gemini', models: [{ family: 'gemini' }], inputSchema: { /* ... */ } }, MyGeminiSearchTool ); ``` When `overridesTool` is set: - The model-specific tool is **not** individually selectable in the UI - It automatically replaces the base tool when enabled and the model matches - The base tool must be registered and enabled for the override to work ### Read the prompt Read the prompt. There is no replacement for just using your tool a lot, and reading the prompt. Read the whole thing top to bottom. What story does it tell? Get familiar with the prompt as a whole, don't get tunnel vision for one message. Does your new tool result make sense to you as a human? Is it formatted in a way that's consistent with other tool results and context in the user message? ![](./media/debug-view.png) ![](./media/tool-log.png) ================================================ FILE: eslint.config.mjs ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ // @ts-check import stylisticEslint from '@stylistic/eslint-plugin'; import tsEslint from '@typescript-eslint/eslint-plugin'; import tsParser from '@typescript-eslint/parser'; import importEslint from 'eslint-plugin-import'; import jsdocEslint from 'eslint-plugin-jsdoc'; import fs from 'fs'; import { builtinModules } from 'module'; import path from 'path'; import tseslint from 'typescript-eslint'; import { fileURLToPath } from 'url'; import headerEslint from 'eslint-plugin-header'; headerEslint.rules.header.meta.schema = false; import * as localEslint from './.eslintplugin/index.ts'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ignores = fs.readFileSync(path.join(__dirname, '.eslint-ignore'), 'utf8') .toString() .split(/\r\n|\n/) .filter(line => line && !line.startsWith('#')); export default tseslint.config( // Global ignores { ignores: [ ...ignores, '!**/.eslint-plugin-local/**/*' ], }, // All js/ts files { files: [ '**/*.{js,jsx,mjs,cjs,ts,tsx}', ], ignores: [ './src/extension/completions-core/**/testdata/*', ], languageOptions: { parser: tsParser, }, plugins: { '@stylistic': stylisticEslint, 'header': headerEslint, }, rules: { 'indent': [ 'error', 'tab', { ignoredNodes: [ 'SwitchCase', 'ClassDeclaration', 'TemplateLiteral *', // Conflicts with tsfmt 'CallExpression > ArrowFunctionExpression', // Conflicts with tsfmt 'CallExpression > ArrowFunctionExpression > BlockStatement', // Conflicts with tsfmt 'NewExpression > ArrowFunctionExpression', // Conflicts with tsfmt 'NewExpression > ArrowFunctionExpression > BlockStatement' // Conflicts with tsfmt ] } ], 'constructor-super': 'error', 'curly': 'error', 'eqeqeq': 'error', 'prefer-const': [ 'error', { destructuring: 'all' } ], 'no-buffer-constructor': 'error', 'no-caller': 'error', 'no-case-declarations': 'error', 'no-debugger': 'error', 'no-duplicate-case': 'error', 'no-duplicate-imports': 'error', 'no-eval': 'error', 'no-async-promise-executor': 'error', 'no-extra-semi': 'error', 'no-new-wrappers': 'error', 'no-redeclare': 'off', 'no-sparse-arrays': 'error', 'no-throw-literal': 'error', 'no-unsafe-finally': 'error', 'no-unused-labels': 'error', 'no-restricted-globals': [ 'error', 'name', 'length', 'event', 'closed', 'external', 'status', 'origin', 'orientation', 'context' ], // non-complete list of globals that are easy to access unintentionally 'no-var': 'error', 'semi': 'error', 'header/header': [ 'error', 'block', [ '---------------------------------------------------------------------------------------------', ' * Copyright (c) Microsoft Corporation. All rights reserved.', ' * Licensed under the MIT License. See License.txt in the project root for license information.', ' *--------------------------------------------------------------------------------------------' ] ] }, settings: { 'import/resolver': { typescript: { extensions: ['.ts', '.tsx'] } } }, }, // All ts files { files: [ '**/*.{ts,tsx}', ], languageOptions: { parser: tsParser, }, plugins: { '@typescript-eslint': tsEslint, '@stylistic': stylisticEslint, 'jsdoc': jsdocEslint, }, rules: { 'jsdoc/no-types': 'error', '@stylistic/member-delimiter-style': 'error', '@typescript-eslint/naming-convention': [ 'error', { selector: 'class', format: ['PascalCase'] } ], }, settings: { 'import/resolver': { typescript: { extensions: ['.ts', '.tsx'] } } }, }, // Main extension sources { files: [ 'src/**/*.{ts,tsx}', 'test/**/*.{ts,tsx}', ], ignores: [ '**/.esbuild.ts', './src/extension/completions-core/vscode-node/bridge/src/completionsTelemetryServiceBridge.ts', ], languageOptions: { parser: tseslint.parser, }, plugins: { 'import': importEslint, 'local': localEslint, }, rules: { 'no-restricted-imports': [ 'error', // node: builtins ...builtinModules, // node: dependencies '@humanwhocodes/gitignore-to-minimatch', '@vscode/extension-telemetry', 'applicationinsights', 'ignore', 'isbinaryfile', 'minimatch', 'source-map-support', 'vscode-tas-client', 'web-tree-sitter' ], 'import/no-restricted-paths': [ 'error', { zones: [ { target: '**/common/**', from: [ '**/vscode/**', '**/node/**', '**/vscode-node/**', '**/worker/**', '**/vscode-worker/**' ] }, { target: '**/vscode/**', from: [ '**/node/**', '**/vscode-node/**', '**/worker/**', '**/vscode-worker/**' ] }, { target: '**/node/**', from: [ '**/vscode/**', '**/vscode-node/**', '**/worker/**', '**/vscode-worker/**' ] }, { target: '**/vscode-node/**', from: [ '**/worker/**', '**/vscode-worker/**' ] }, { target: '**/worker/**', from: [ '**/vscode/**', '**/node/**', '**/vscode-node/**', '**/vscode-worker/**' ] }, { target: '**/vscode-worker/**', from: [ '**/node/**', '**/vscode-node/**' ] }, { target: './src/', from: './test/' }, { target: './src/util', from: ['./src/platform', './src/extension'] }, { target: './src/platform', from: ['./src/extension'] }, { target: ['./test', '!./test/base/extHostContext/*.ts'], from: ['**/vscode-node/**', '**/vscode-worker/**'] }, { target: 'src/!(lib)/**', from: './src/lib' } ] } ], 'local/no-instanceof-uri': ['error'], 'local/no-test-imports': ['error'], 'local/no-runtime-import': [ 'error', { test: ['vscode'], 'src/**/common/**/*': ['vscode'], 'src/**/node/**/*': ['vscode'] } ], 'local/no-funny-filename': ['error'], 'local/no-bad-gdpr-comment': ['error'], 'local/no-gdpr-event-name-mismatch': ['error'], 'local/no-unlayered-files': ['error'], 'local/no-restricted-copilot-pr-string': [ 'error', { className: 'GitHubPullRequestProviders', string: 'Generate with Copilot' } ], 'local/no-nls-localize': ['error'], 'local/no-unexternalized-strings': ['error'], } }, { files: ['**/{vscode-node,node}/**/*.ts', '**/{vscode-node,node}/**/*.tsx'], rules: { 'no-restricted-imports': 'off' } }, { files: ['**/*.js'], rules: { 'jsdoc/no-types': 'off' } }, { files: ['src/extension/**/*.tsx'], rules: { 'local/no-missing-linebreak': 'error' } }, { files: ['**/*.test.ts', '**/*.test.tsx'], rules: { 'local/no-test-only': 'error' } }, { files: [ 'test/**', 'src/vscodeTypes.ts', 'script/**', 'src/extension/*.d.ts', 'build/**' ], rules: { 'local/no-unlayered-files': 'off', 'no-restricted-imports': 'off' } }, // no-explicit-any { files: [ 'src/**/*.ts', ], ignores: [ 'src/util/vs/**/*.ts', // vendored code 'src/**/*.spec.ts', // allow in tests './src/extension/agents/copilotcli/node/nodePtyShim.ts', './src/extension/byok/common/anthropicMessageConverter.ts', './src/extension/byok/common/geminiFunctionDeclarationConverter.ts', './src/extension/byok/common/geminiMessageConverter.ts', './src/extension/byok/vscode-node/anthropicProvider.ts', './src/extension/byok/vscode-node/geminiNativeProvider.ts', './src/extension/byok/vscode-node/ollamaProvider.ts', './src/extension/chatSessions/vscode-node/copilotCloudSessionContentBuilder.ts', './src/extension/chatSessions/vscode-node/copilotCloudSessionsProvider.ts', './src/extension/codeBlocks/node/codeBlockProcessor.ts', './src/extension/codeBlocks/vscode-node/provider.ts', './src/extension/configuration/vscode-node/configurationMigration.ts', './src/extension/context/node/resolvers/genericInlineIntentInvocation.ts', './src/extension/context/node/resolvers/genericPanelIntentInvocation.ts', './src/extension/context/node/resolvers/inlineFixIntentInvocation.ts', './src/extension/context/node/resolvers/promptWorkspaceLabels.ts', './src/extension/contextKeys/vscode-node/contextKeys.contribution.ts', './src/extension/conversation/vscode-node/userActions.ts', './src/extension/extension/vscode/services.ts', './src/extension/inlineChat/node/rendererVisualization.ts', './src/extension/inlineChat/vscode-node/inlineChatCommands.ts', './src/extension/inlineEdits/common/observableWorkspaceRecordingReplayer.ts', './src/extension/inlineEdits/vscode-node/parts/vscodeWorkspace.ts', './src/extension/intents/node/editCodeIntent.ts', './src/extension/intents/node/editCodeStep.ts', './src/extension/intents/node/fixIntent.ts', './src/extension/intents/node/newIntent.ts', './src/extension/intents/node/searchIntent.ts', './src/extension/languageContextProvider/vscode-node/languageContextProviderService.ts', './src/extension/linkify/common/commands.ts', './src/extension/linkify/common/responseStreamWithLinkification.ts', './src/extension/linkify/test/node/util.ts', './src/extension/log/vscode-node/loggingActions.ts', './src/extension/log/vscode-node/requestLogTree.ts', './src/extension/mcp/test/vscode-node/util.ts', './src/extension/mcp/vscode-node/commands.ts', './src/extension/mcp/vscode-node/nuget.ts', './src/extension/onboardDebug/node/copilotDebugWorker/rpc.ts', './src/extension/onboardDebug/node/parseLaunchConfigFromResponse.ts', './src/extension/onboardDebug/vscode-node/copilotDebugCommandHandle.ts', './src/extension/prompt/common/toolCallRound.ts', './src/extension/prompt/node/chatMLFetcher.ts', './src/extension/prompt/node/chatParticipantTelemetry.ts', './src/extension/prompt/node/editGeneration.ts', './src/extension/prompt/node/intents.ts', './src/extension/prompt/node/todoListContextProvider.ts', './src/extension/prompt/vscode-node/endpointProviderImpl.ts', './src/extension/prompt/vscode-node/requestLoggerImpl.ts', './src/extension/prompts/node/agent/promptRegistry.ts', './src/extension/prompts/node/base/promptElement.ts', './src/extension/prompts/node/base/promptRenderer.ts', './src/extension/prompts/node/test/utils.ts', './src/extension/replay/common/chatReplayResponses.ts', './src/extension/replay/node/replayParser.ts', './src/extension/replay/vscode-node/replayDebugSession.ts', './src/extension/review/node/githubReviewAgent.ts', './src/extension/test/node/services.ts', './src/extension/test/vscode-node/extension.test.ts', './src/extension/test/vscode-node/sanity.sanity-test.ts', './src/extension/test/vscode-node/session.test.ts', './src/extension/tools/common/toolSchemaNormalizer.ts', './src/extension/tools/common/toolsService.ts', './src/extension/typescriptContext/common/serverProtocol.ts', './src/extension/typescriptContext/serverPlugin/src/common/baseContextProviders.ts', './src/extension/typescriptContext/serverPlugin/src/common/contextProvider.ts', './src/extension/typescriptContext/serverPlugin/src/common/protocol.ts', './src/extension/typescriptContext/serverPlugin/src/common/typescripts.ts', './src/extension/typescriptContext/serverPlugin/src/common/utils.ts', './src/extension/typescriptContext/vscode-node/inspector.ts', './src/extension/typescriptContext/vscode-node/languageContextService.ts', './src/extension/workspaceRecorder/vscode-node/workspaceListenerService.ts', './src/extension/workspaceSemanticSearch/node/semanticSearchTextSearchProvider.ts', './src/lib/node/chatLibMain.ts', './src/platform/authentication/test/node/simulationTestCopilotTokenManager.ts', './src/platform/chat/common/blockedExtensionService.ts', './src/platform/chunking/common/chunkingEndpointClientImpl.ts', './src/platform/commands/common/mockRunCommandExecutionService.ts', './src/platform/commands/common/runCommandExecutionService.ts', './src/platform/commands/vscode/runCommandExecutionServiceImpl.ts', './src/platform/configuration/common/configurationService.ts', './src/platform/configuration/common/validator.ts', './src/platform/configuration/test/common/inMemoryConfigurationService.ts', './src/platform/configuration/vscode/configurationServiceImpl.ts', './src/platform/customInstructions/common/customInstructionsService.ts', './src/platform/debug/vscode/debugOutputListener.ts', './src/platform/diff/node/diffWorkerMain.ts', './src/platform/editing/common/notebookDocumentSnapshot.ts', './src/platform/editing/common/textDocumentSnapshot.ts', './src/platform/embeddings/common/embeddingsGrouper.ts', './src/platform/embeddings/common/embeddingsIndex.ts', './src/platform/embeddings/common/remoteEmbeddingsComputer.ts', './src/platform/endpoint/node/modelMetadataFetcher.ts', './src/platform/endpoint/test/node/openaiCompatibleEndpoint.ts', './src/platform/env/common/packagejson.ts', './src/platform/extensions/common/extensionsService.ts', './src/platform/filesystem/common/fileSystemService.ts', './src/platform/github/common/githubService.ts', './src/platform/github/common/nullOctokitServiceImpl.ts', './src/platform/inlineEdits/common/dataTypes/edit.ts', './src/platform/inlineEdits/common/dataTypes/textEditLengthHelper/length.ts', './src/platform/inlineEdits/common/editReason.ts', './src/platform/inlineEdits/common/statelessNextEditProvider.ts', './src/platform/inlineEdits/common/utils/observable.ts', './src/platform/languages/common/languageDiagnosticsService.ts', './src/platform/log/common/logExecTime.ts', './src/platform/log/common/logService.ts', './src/platform/log/vscode/outputChannelLogTarget.ts', './src/platform/nesFetch/common/completionsFetchService.ts', './src/platform/nesFetch/node/completionsFetchServiceImpl.ts', './src/platform/networking/common/fetch.ts', './src/platform/networking/common/fetcherService.ts', './src/platform/networking/common/networking.ts', './src/platform/networking/common/openai.ts', './src/platform/networking/node/baseFetchFetcher.ts', './src/platform/networking/node/chatStream.ts', './src/platform/networking/node/fetcherFallback.ts', './src/platform/networking/node/nodeFetchFetcher.ts', './src/platform/networking/node/nodeFetcher.ts', './src/platform/networking/node/stream.ts', './src/platform/networking/node/test/nodeFetcherService.ts', './src/platform/networking/vscode-node/electronFetcher.ts', './src/platform/networking/vscode-node/fetcherServiceImpl.ts', './src/platform/notification/common/notificationService.ts', './src/platform/notification/vscode/notificationServiceImpl.ts', './src/platform/openai/node/fetch.ts', './src/platform/parser/node/nodes.ts', './src/platform/parser/node/parserServiceImpl.ts', './src/platform/parser/node/parserWorker.ts', './src/platform/parser/node/treeSitterQueries.ts', './src/platform/remoteCodeSearch/common/githubCodeSearchService.ts', './src/platform/remoteSearch/node/codeOrDocsSearchClientImpl.ts', './src/platform/review/vscode/reviewServiceImpl.ts', './src/platform/scopeSelection/vscode-node/scopeSelectionImpl.ts', './src/platform/snippy/common/snippyTypes.ts', './src/platform/survey/vscode/surveyServiceImpl.ts', './src/platform/tasks/vscode/tasksService.ts', './src/platform/telemetry/common/failingTelemetryReporter.ts', './src/platform/telemetry/common/telemetryData.ts', './src/platform/telemetry/node/azureInsightsReporter.ts', './src/platform/telemetry/node/spyingTelemetryService.ts', './src/platform/terminal/common/terminalService.ts', './src/platform/terminal/vscode/terminalServiceImpl.ts', './src/platform/test/common/endpointTestFixtures.ts', './src/platform/test/common/testExtensionsService.ts', './src/platform/test/node/extensionContext.ts', './src/platform/test/node/fetcher.ts', './src/platform/test/node/services.ts', './src/platform/test/node/simulationWorkspace.ts', './src/platform/test/node/telemetry.ts', './src/platform/test/node/testWorkbenchService.ts', './src/platform/testing/common/nullWorkspaceMutationManager.ts', './src/platform/tfidf/node/tfidf.ts', './src/platform/tfidf/node/tfidfMessaging.ts', './src/platform/tfidf/node/tfidfWorker.ts', './src/platform/thinking/common/thinking.ts', './src/platform/tokenizer/node/tikTokenizerWorker.ts', './src/platform/tokenizer/node/tokenizer.ts', './src/platform/workbench/common/workbenchService.ts', './src/platform/workbench/vscode/workbenchServiceImpt.ts', './src/platform/workspaceChunkSearch/node/nullWorkspaceFileIndex.ts', './src/platform/workspaceChunkSearch/node/tfidfChunkSearch.ts', './src/platform/workspaceChunkSearch/node/workspaceFileIndex.ts', './src/platform/workspaceRecorder/common/resolvedRecording/resolvedRecording.ts', './src/util/common/async.ts', './src/util/common/cache.ts', './src/util/common/chatResponseStreamImpl.ts', './src/util/common/debounce.ts', './src/util/common/debugValueEditorGlobals.ts', './src/util/common/diff.ts', './src/util/common/progress.ts', './src/util/common/test/shims/chatTypes.ts', './src/util/common/test/shims/editing.ts', './src/util/common/test/shims/l10n.ts', './src/util/common/test/shims/notebookDocument.ts', './src/util/common/test/shims/vscodeTypesShim.ts', './src/util/common/test/simpleMock.ts', './src/util/common/timeTravelScheduler.ts', './src/util/common/types.ts', './src/util/node/worker.ts', ], languageOptions: { parser: tseslint.parser, }, plugins: { '@typescript-eslint': tseslint.plugin, }, rules: { '@typescript-eslint/no-explicit-any': [ 'warn', { 'fixToUnknown': true } ] } }, { files: ['./src/lib/node/chatLibMain.ts'], rules: { 'import/no-restricted-paths': 'off' } }, ); ================================================ FILE: lint-staged.config.js ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ const ESLint = require('eslint').ESLint; const removeIgnoredFiles = async (files) => { const eslint = new ESLint(); const isIgnored = await Promise.all( files.map((file) => { return eslint.isPathIgnored(file); }) ); const filteredFiles = files.filter((_, i) => !isIgnored[i]); return filteredFiles.join(' '); }; module.exports = { '!({.esbuild.ts,test/simulation/fixtures/**,test/scenarios/**,.vscode/extensions/**,**/vscode.proposed.*})*{.ts,.js,.tsx}': async (files) => { const filesToLint = await removeIgnoredFiles(files); if (!filesToLint) { return []; } return [ `npm run tsfmt -- ${filesToLint}`, `eslint --max-warnings=0 ${filesToLint}` ]; }, }; ================================================ FILE: package.json ================================================ { "name": "copilot-chat", "displayName": "GitHub Copilot Chat", "description": "AI chat features powered by Copilot", "version": "0.41.0", "build": "1", "internalAIKey": "1058ec22-3c95-4951-8443-f26c1f325911", "completionsCoreVersion": "1.378.1799", "internalLargeStorageAriaKey": "ec712b3202c5462fb6877acae7f1f9d7-c19ad55e-3e3c-4f99-984b-827f6d95bd9e-6917", "ariaKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255", "buildType": "dev", "publisher": "GitHub", "homepage": "https://github.com/features/copilot?editor=vscode", "license": "SEE LICENSE IN LICENSE.txt", "repository": { "type": "git", "url": "https://github.com/microsoft/vscode-copilot-chat" }, "bugs": { "url": "https://github.com/microsoft/vscode/issues" }, "qna": "https://github.com/github-community/community/discussions/categories/copilot", "icon": "assets/copilot.png", "pricing": "Trial", "engines": { "vscode": "^1.111.0", "npm": ">=9.0.0", "node": ">=22.14.0" }, "categories": [ "AI", "Chat", "Programming Languages", "Machine Learning" ], "keywords": [ "ai", "openai", "codex", "pilot", "snippets", "documentation", "autocomplete", "intellisense", "refactor", "javascript", "python", "typescript", "php", "go", "golang", "ruby", "c++", "c#", "java", "kotlin", "co-pilot" ], "badges": [ { "url": "https://img.shields.io/badge/GitHub%20Copilot-Subscription%20Required-orange", "href": "https://github.com/github-copilot/signup?editor=vscode", "description": "%github.copilot.badge.signUp%" }, { "url": "https://img.shields.io/github/stars/github/copilot-docs?style=social", "href": "https://github.com/github/copilot-docs", "description": "%github.copilot.badge.star%" }, { "url": "https://img.shields.io/youtube/channel/views/UC7c3Kb6jYCRj4JOHHZTxKsQ?style=social", "href": "https://www.youtube.com/@GitHub/search?query=copilot", "description": "%github.copilot.badge.youtube%" }, { "url": "https://img.shields.io/twitter/follow/github?style=social", "href": "https://twitter.com/github", "description": "%github.copilot.badge.twitter%" } ], "activationEvents": [ "onStartupFinished", "onLanguageModelChat:copilot", "onUri", "onFileSystem:ccreq", "onFileSystem:ccsettings" ], "main": "./dist/extension", "l10n": "./l10n", "enabledApiProposals": [ "agentSessionsWorkspace", "chatDebug@4", "chatHooks@6", "extensionsAny", "newSymbolNamesProvider", "interactive", "codeActionAI", "activeComment", "commentReveal", "contribCommentThreadAdditionalMenu", "contribCommentsViewThreadMenus", "contribChatEditorInlineGutterMenu", "documentFiltersExclusive", "embeddings", "findTextInFiles", "findTextInFiles2", "languageModelToolSupportsModel@1", "findFiles2@2", "textSearchProvider", "terminalDataWriteEvent", "terminalExecuteCommandEvent", "terminalSelection", "terminalQuickFixProvider", "mappedEditsProvider", "aiRelatedInformation", "aiSettingsSearch", "chatParticipantAdditions@3", "defaultChatParticipant@4", "contribSourceControlInputBoxMenu", "authLearnMore", "testObserver", "aiTextSearchProvider@2", "chatParticipantPrivate@15", "chatProvider@4", "contribDebugCreateConfiguration", "chatReferenceDiagnostic", "textSearchProvider2", "chatReferenceBinaryData", "languageModelSystem", "languageModelCapabilities", "inlineCompletionsAdditions", "chatStatusItem", "taskProblemMatcherStatus", "contribLanguageModelToolSets", "textDocumentChangeReason", "resolvers", "taskExecutionTerminal", "dataChannels", "languageModelThinkingPart", "chatSessionsProvider@3", "devDeviceId", "contribEditorContentMenu", "chatPromptFiles", "mcpServerDefinitions", "tabInputMultiDiff", "workspaceTrust", "environmentPower", "terminalTitle" ], "contributes": { "languageModelTools": [ { "name": "copilot_searchCodebase", "toolReferenceName": "codebase", "displayName": "%copilot.tools.searchCodebase.name%", "icon": "$(folder)", "userDescription": "%copilot.codebase.tool.description%", "modelDescription": "Run a natural language search for relevant code or documentation comments from the user's current workspace. Returns relevant code snippets from the user's current workspace if it is large, or the full contents of the workspace if it is small.", "tags": [ "codesearch", "vscode_codesearch" ], "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "The query to search the codebase for. Should contain all relevant context. Should ideally be text that might appear in the codebase, such as function names, variable names, or comments." } }, "required": [ "query" ] } }, { "name": "execution_subagent", "toolReferenceName": "executionSubagent", "displayName": "%copilot.tools.executionSubagent.name%", "icon": "$(play)", "canBeReferencedInPrompt": true, "userDescription": "%copilot.tools.executionSubagent.description%", "modelDescription": "Launch an iterative execution-focused subagent that performs an execution-based task.\nUSE THIS INSTEAD OF RUNNING INDIVIDUAL COMMANDS WITH run_in_terminal EXCEPT IN THE RARE CASES THAT YOU NEED THE FULL OUTPUT OF A COMMAND.\nHere are some examples of how it can be used:\n- Run tests and filter the output to summarize which tests failed and why.\n- Install all dependencies of a project.\nReturns: A list of commands that were run, along with relevant excerpts of each command's output.\nInput fields:\n- query: What to execute, and what to look for in the output. Can include exact commands to run, or a description of an execution task.\n- description: Short user-visible invocation message.\nNOTE: In the subagent query, make sure to specify any restrictions or guidelines on running commands provided by the user earlier in the conversation.\nFor example, if the user instructs the agent to not edit files in a particular directory, make sure to include that instruction in the subagent query when relevant.", "when": "config.github.copilot.chat.enableExecutionSubagent", "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "What to execute, and what to look for in the output. Can include exact commands to run, or a description of an execution task." }, "description": { "type": "string", "description": "User-visible invocation message shown while the subagent runs." } }, "required": [ "query", "description" ] } }, { "name": "search_subagent", "toolReferenceName": "searchSubagent", "displayName": "%copilot.tools.searchSubagent.name%", "icon": "$(search)", "userDescription": "%copilot.tools.searchSubagent.description%", "modelDescription": "Launch a fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. \"src/components/**/*.tsx\"), search code for keywords (eg. \"API endpoints\"), or answer questions about the codebase (eg. \"how do API endpoints work?\").\nReturns: A list of relevant files/snippet locations in the workspace.\n\nInput fields:\n- query: Natural language description of what to search for.\n- description: Short user-visible invocation message. \n- details: 2-3 sentences detailing the objective of the search agent.", "when": "config.github.copilot.chat.searchSubagent.enabled", "tags": [ "vscode_codesearch" ], "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "Natural language description of what to search for." }, "description": { "type": "string", "description": "A short (3-5 word) description of the task." }, "details": { "type": "string", "description": "A more detailed description of the objective for the search subagent. This helps the sub-agent remain on task and understand its purpose." } }, "required": [ "query", "description", "details" ] } }, { "name": "copilot_searchWorkspaceSymbols", "toolReferenceName": "symbols", "displayName": "%copilot.tools.searchWorkspaceSymbols.name%", "icon": "$(symbol)", "userDescription": "%copilot.workspaceSymbols.tool.description%", "modelDescription": "Search the user's workspace for code symbols using language services. Use this tool when the user is looking for a specific symbol in their workspace.", "tags": [ "vscode_codesearch" ], "inputSchema": { "type": "object", "properties": { "symbolName": { "type": "string", "description": "The symbol to search for, such as a function name, class name, or variable name." } }, "required": [ "symbolName" ] } }, { "name": "copilot_getVSCodeAPI", "toolReferenceName": "vscodeAPI", "displayName": "%copilot.tools.getVSCodeAPI.name%", "icon": "$(references)", "userDescription": "%copilot.vscode.tool.description%", "modelDescription": "Get comprehensive VS Code API documentation and references for extension development. This tool provides authoritative documentation for VS Code's extensive API surface, including proposed APIs, contribution points, and best practices. Use this tool for understanding complex VS Code API interactions.\n\nWhen to use this tool:\n- User asks about specific VS Code APIs, interfaces, or extension capabilities\n- Need documentation for VS Code extension contribution points (commands, views, settings, etc.)\n- Questions about proposed APIs and their usage patterns\n- Understanding VS Code extension lifecycle, activation events, and packaging\n- Best practices for VS Code extension development architecture\n- API examples and code patterns for extension features\n- Troubleshooting extension-specific issues or API limitations\n\nWhen NOT to use this tool:\n- Creating simple standalone files or scripts unrelated to VS Code extensions\n- General programming questions not specific to VS Code extension development\n- Questions about using VS Code as an editor (user-facing features)\n- Non-extension related development tasks\n- File creation or editing that doesn't involve VS Code extension APIs\n\nCRITICAL usage guidelines:\n1. Always include specific API names, interfaces, or concepts in your query\n2. Mention the extension feature you're trying to implement\n3. Include context about proposed vs stable APIs when relevant\n4. Reference specific contribution points when asking about extension manifest\n5. Be specific about the VS Code version or API version when known\n\nScope: This tool is for EXTENSION DEVELOPMENT ONLY - building tools that extend VS Code itself, not for general file creation or non-extension programming tasks.", "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "The query to search vscode documentation for. Should contain all relevant context." } }, "required": [ "query" ] }, "tags": [] }, { "name": "copilot_findFiles", "toolReferenceName": "fileSearch", "displayName": "%copilot.tools.findFiles.name%", "userDescription": "%copilot.tools.findFiles.userDescription%", "modelDescription": "Search for files in the workspace by glob pattern. This only returns the paths of matching files. Use this tool when you know the exact filename pattern of the files you're searching for. Glob patterns match from the root of the workspace folder. Examples:\n- **/*.{js,ts} to match all js/ts files in the workspace.\n- src/** to match all files under the top-level src folder.\n- **/foo/**/*.js to match all js files under any foo folder in the workspace.\n\nIn a multi-root workspace, you can scope the search to a specific workspace folder by using the absolute path to the folder as the query, e.g. /path/to/folder/**/*.ts.", "tags": [ "vscode_codesearch" ], "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "Search for files with names or paths matching this glob pattern. Can also be an absolute path to a workspace folder to scope the search in a multi-root workspace." }, "maxResults": { "type": "number", "description": "The maximum number of results to return. Do not use this unless necessary, it can slow things down. By default, only some matches are returned. If you use this and don't see what you're looking for, you can try again with a more specific query or a larger maxResults." } }, "required": [ "query" ] } }, { "name": "copilot_findTextInFiles", "toolReferenceName": "textSearch", "displayName": "%copilot.tools.findTextInFiles.name%", "userDescription": "%copilot.tools.findTextInFiles.userDescription%", "modelDescription": "Do a fast text search in the workspace. Use this tool when you want to search with an exact string or regex. If you are not sure what words will appear in the workspace, prefer using regex patterns with alternation (|) or character classes to search for multiple potential words at once instead of making separate searches. For example, use 'function|method|procedure' to look for all of those words at once. Use includePattern to search within files matching a specific pattern, or in a specific file, using a relative path. Use 'includeIgnoredFiles' to include files normally ignored by .gitignore, other ignore files, and `files.exclude` and `search.exclude` settings. Warning: using this may cause the search to be slower, only set it when you want to search in ignored folders like node_modules or build outputs. Use this tool when you want to see an overview of a particular file, instead of using read_file many times to look for code within a file.\n\nIn a multi-root workspace, you can scope the search to a specific workspace folder by using the absolute path to the folder as the includePattern, e.g. /path/to/folder.", "tags": [ "vscode_codesearch" ], "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "The pattern to search for in files in the workspace. Use regex with alternation (e.g., 'word1|word2|word3') or character classes to find multiple potential words in a single search. Be sure to set the isRegexp property properly to declare whether it's a regex or plain text pattern. Is case-insensitive." }, "isRegexp": { "type": "boolean", "description": "Whether the pattern is a regex." }, "includePattern": { "type": "string", "description": "Search files matching this glob pattern. Will be applied to the relative path of files within the workspace. To search recursively inside a folder, use a proper glob pattern like \"src/folder/**\". Do not use | in includePattern. Can also be an absolute path to a workspace folder to scope the search in a multi-root workspace." }, "maxResults": { "type": "number", "description": "The maximum number of results to return. Do not use this unless necessary, it can slow things down. By default, only some matches are returned. If you use this and don't see what you're looking for, you can try again with a more specific query or a larger maxResults." }, "includeIgnoredFiles": { "type": "boolean", "description": "Whether to include files that would normally be ignored according to .gitignore, other ignore files and `files.exclude` and `search.exclude` settings. Warning: using this may cause the search to be slower. Only set it when you want to search in ignored folders like node_modules or build outputs." } }, "required": [ "query", "isRegexp" ] } }, { "name": "copilot_applyPatch", "displayName": "%copilot.tools.applyPatch.name%", "toolReferenceName": "applyPatch", "userDescription": "%copilot.tools.applyPatch.description%", "modelDescription": "Edit text files. Do not use this tool to edit Jupyter notebooks. `apply_patch` allows you to execute a diff/patch against a text file, but the format of the diff specification is unique to this task, so pay careful attention to these instructions. To use the `apply_patch` command, you should pass a message of the following structure as \"input\":\n\n*** Begin Patch\n[YOUR_PATCH]\n*** End Patch\n\nWhere [YOUR_PATCH] is the actual content of your patch, specified in the following V4A diff format.\n\n*** [ACTION] File: [/absolute/path/to/file] -> ACTION can be one of Add, Update, or Delete.\nAn example of a message that you might pass as \"input\" to this function, in order to apply a patch, is shown below.\n\n*** Begin Patch\n*** Update File: /Users/someone/pygorithm/searching/binary_search.py\n@@class BaseClass\n@@ def search():\n- pass\n+ raise NotImplementedError()\n\n@@class Subclass\n@@ def search():\n- pass\n+ raise NotImplementedError()\n\n*** End Patch\nDo not use line numbers in this diff format.", "inputSchema": { "type": "object", "properties": { "input": { "type": "string", "description": "The edit patch to apply." }, "explanation": { "type": "string", "description": "A short description of what the tool call is aiming to achieve." } }, "required": [ "input", "explanation" ] } }, { "name": "copilot_readFile", "toolReferenceName": "readFile", "legacyToolReferenceFullNames": [ "search/readFile" ], "displayName": "%copilot.tools.readFile.name%", "userDescription": "%copilot.tools.readFile.userDescription%", "modelDescription": "Read the contents of a file.\n\nYou must specify the line range you're interested in. Line numbers are 1-indexed. If the file contents returned are insufficient for your task, you may call this tool again to retrieve more content. Prefer reading larger ranges over doing many small reads. Binary files use startLine/endLine as byte offsets.", "tags": [ "vscode_codesearch" ], "inputSchema": { "type": "object", "properties": { "filePath": { "description": "The absolute path of the file to read.", "type": "string" }, "startLine": { "type": "number", "description": "The line number to start reading from, 1-based." }, "endLine": { "type": "number", "description": "The inclusive line number to end reading at, 1-based." } }, "required": [ "filePath", "startLine", "endLine" ] } }, { "name": "copilot_viewImage", "toolReferenceName": "viewImage", "displayName": "%copilot.tools.viewImage.name%", "userDescription": "%copilot.tools.viewImage.userDescription%", "when": "config.github.copilot.chat.tools.viewImage.enabled", "modelDescription": "View the contents of an image file. Use this instead of read_file for supported image files such as png, jpg, jpeg, gif, and webp. The tool returns the image directly to multimodal models and does not take line ranges or offsets.", "inputSchema": { "type": "object", "properties": { "filePath": { "description": "The absolute path of the image file to view.", "type": "string" } }, "required": [ "filePath" ] } }, { "name": "copilot_listDirectory", "toolReferenceName": "listDirectory", "displayName": "%copilot.tools.listDirectory.name%", "userDescription": "%copilot.tools.listDirectory.userDescription%", "modelDescription": "List the contents of a directory. Result will have the name of the child. If the name ends in /, it's a folder, otherwise a file", "tags": [ "vscode_codesearch" ], "inputSchema": { "type": "object", "properties": { "path": { "type": "string", "description": "The absolute path to the directory to list." } }, "required": [ "path" ] } }, { "name": "copilot_getErrors", "displayName": "%copilot.tools.getErrors.name%", "toolReferenceName": "problems", "legacyToolReferenceFullNames": [ "problems" ], "icon": "$(error)", "userDescription": "%copilot.tools.errors.description%", "modelDescription": "Get any compile or lint errors in a specific file or across all files. If the user mentions errors or problems in a file, they may be referring to these. Use the tool to see the same errors that the user is seeing. If the user asks you to analyze all errors, or does not specify a file, use this tool to gather errors for all files. Also use this tool after editing a file to validate the change.", "tags": [], "inputSchema": { "type": "object", "properties": { "filePaths": { "description": "The absolute paths to the files or folders to check for errors. Omit 'filePaths' when retrieving all errors.", "type": "array", "items": { "type": "string" } } } } }, { "name": "copilot_readProjectStructure", "displayName": "%copilot.tools.readProjectStructure.name%", "modelDescription": "Get a file tree representation of the workspace.", "tags": [] }, { "name": "copilot_getChangedFiles", "displayName": "%copilot.tools.getChangedFiles.name%", "toolReferenceName": "changes", "legacyToolReferenceFullNames": [ "changes" ], "icon": "$(diff)", "userDescription": "%copilot.tools.changes.description%", "modelDescription": "Get git diffs of current file changes in a git repository. Don't forget that you can use run_in_terminal to run git commands in a terminal as well.", "tags": [ "vscode_codesearch" ], "inputSchema": { "type": "object", "properties": { "repositoryPath": { "type": "string", "description": "The absolute path to the git repository to look for changes in. If not provided, the active git repository will be used." }, "sourceControlState": { "type": "array", "items": { "type": "string", "enum": [ "staged", "unstaged", "merge-conflicts" ] }, "description": "The kinds of git state to filter by. Allowed values are: 'staged', 'unstaged', and 'merge-conflicts'. If not provided, all states will be included." } } } }, { "name": "copilot_testFailure", "toolReferenceName": "testFailure", "legacyToolReferenceFullNames": [ "testFailure" ], "displayName": "%copilot.tools.testFailure.name%", "icon": "$(beaker)", "userDescription": "%copilot.testFailure.tool.description%", "modelDescription": "Includes test failure information in the prompt.", "inputSchema": {}, "tags": [ "vscode_editing_with_tests", "enable_other_tool_copilot_readFile", "enable_other_tool_copilot_listDirectory", "enable_other_tool_copilot_findFiles", "enable_other_tool_copilot_runTests" ] }, { "name": "copilot_createNewWorkspace", "displayName": "%github.copilot.tools.createNewWorkspace.name%", "toolReferenceName": "newWorkspace", "legacyToolReferenceFullNames": [ "new/newWorkspace" ], "icon": "$(new-folder)", "userDescription": "%github.copilot.tools.createNewWorkspace.userDescription%", "when": "config.github.copilot.chat.newWorkspaceCreation.enabled", "modelDescription": "Get comprehensive setup steps to help the user create complete project structures in a VS Code workspace. This tool is designed for full project initialization and scaffolding, not for creating individual files.\n\nWhen to use this tool:\n- User wants to create a new complete project from scratch\n- Setting up entire project frameworks (TypeScript projects, React apps, Node.js servers, etc.)\n- Initializing Model Context Protocol (MCP) servers with full structure\n- Creating VS Code extensions with proper scaffolding\n- Setting up Next.js, Vite, or other framework-based projects\n- User asks for \"new project\", \"create a workspace\", \"set up a [framework] project\"\n- Need to establish complete development environment with dependencies, config files, and folder structure\n\nWhen NOT to use this tool:\n- Creating single files or small code snippets\n- Adding individual files to existing projects\n- Making modifications to existing codebases\n- User asks to \"create a file\" or \"add a component\"\n- Simple code examples or demonstrations\n- Debugging or fixing existing code\n\nThis tool provides complete project setup including:\n- Folder structure creation\n- Package.json and dependency management\n- Configuration files (tsconfig, eslint, etc.)\n- Initial boilerplate code\n- Development environment setup\n- Build and run instructions\n\nUse other file creation tools for individual files within existing projects.", "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "The query to use to generate the new workspace. This should be a clear and concise description of the workspace the user wants to create." } }, "required": [ "query" ] }, "tags": [ "enable_other_tool_install_extension", "enable_other_tool_get_project_setup_info" ] }, { "name": "copilot_getProjectSetupInfo", "displayName": "%github.copilot.tools.getProjectSetupInfo.name%", "when": "config.github.copilot.chat.newWorkspaceCreation.enabled && !config.github.copilot.chat.projectSetupInfoSkill.enabled", "toolReferenceName": "getProjectSetupInfo", "legacyToolReferenceFullNames": [ "new/getProjectSetupInfo" ], "modelDescription": "Do not call this tool without first calling the tool to create a workspace. This tool provides a project setup information for a Visual Studio Code workspace based on a project type and programming language.", "inputSchema": { "type": "object", "properties": { "projectType": { "type": "string", "description": "The type of project to create. Supported values are: 'python-script', 'python-project', 'mcp-server', 'model-context-protocol-server', 'vscode-extension', 'next-js', 'vite' and 'other'" } }, "required": [ "projectType" ] }, "tags": [] }, { "name": "copilot_installExtension", "displayName": "Install Extension in VS Code", "when": "!config.github.copilot.chat.installExtensionSkill.enabled", "toolReferenceName": "installExtension", "legacyToolReferenceFullNames": [ "new/installExtension" ], "modelDescription": "Install an extension in VS Code. Use this tool to install an extension in Visual Studio Code as part of a new workspace creation process only.", "inputSchema": { "type": "object", "properties": { "id": { "type": "string", "description": "The ID of the extension to install. This should be in the format .." }, "name": { "type": "string", "description": "The name of the extension to install. This should be a clear and concise description of the extension." } }, "required": [ "id", "name" ] }, "tags": [] }, { "name": "copilot_runVscodeCommand", "displayName": "Run VS Code Command", "when": "config.github.copilot.chat.newWorkspaceCreation.enabled", "toolReferenceName": "runCommand", "legacyToolReferenceFullNames": [ "new/runVscodeCommand" ], "modelDescription": "Run a command in VS Code. Use this tool to run a command in Visual Studio Code as part of a new workspace creation process only.", "inputSchema": { "type": "object", "properties": { "commandId": { "type": "string", "description": "The ID of the command to execute. This should be in the format ." }, "name": { "type": "string", "description": "The name of the command to execute. This should be a clear and concise description of the command." }, "args": { "type": "array", "description": "The arguments to pass to the command. This should be an array of strings.", "items": { "type": "string" } }, "skipCheck": { "type": "boolean", "description": "If true, skip checking whether the command exists before executing it." } }, "required": [ "commandId", "name" ] }, "tags": [] }, { "name": "copilot_createNewJupyterNotebook", "displayName": "Create New Jupyter Notebook", "icon": "$(notebook)", "toolReferenceName": "createJupyterNotebook", "legacyToolReferenceFullNames": [ "newJupyterNotebook" ], "modelDescription": "Generates a new Jupyter Notebook (.ipynb) in VS Code. Jupyter Notebooks are interactive documents commonly used for data exploration, analysis, visualization, and combining code with narrative text. Prefer creating plain Python files or similar unless a user explicitly requests creating a new Jupyter Notebook or already has a Jupyter Notebook opened or exists in the workspace.", "userDescription": "%copilot.tools.newJupyterNotebook.description%", "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "The query to use to generate the jupyter notebook. This should be a clear and concise description of the notebook the user wants to create." } }, "required": [ "query" ] }, "tags": [] }, { "name": "copilot_insertEdit", "toolReferenceName": "insertEdit", "displayName": "%copilot.tools.insertEdit.name%", "modelDescription": "Insert new code into an existing file in the workspace. Use this tool once per file that needs to be modified, even if there are multiple changes for a file. Generate the \"explanation\" property first.\nThe system is very smart and can understand how to apply your edits to the files, you just need to provide minimal hints.\nAvoid repeating existing code, instead use comments to represent regions of unchanged code. Be as concise as possible. For example:\n// ...existing code...\n{ changed code }\n// ...existing code...\n{ changed code }\n// ...existing code...\n\nHere is an example of how you should use format an edit to an existing Person class:\nclass Person {\n\t// ...existing code...\n\tage: number;\n\t// ...existing code...\n\tgetAge() {\n\treturn this.age;\n\t}\n}", "tags": [], "inputSchema": { "type": "object", "properties": { "explanation": { "type": "string", "description": "A short explanation of the edit being made." }, "filePath": { "type": "string", "description": "An absolute path to the file to edit." }, "code": { "type": "string", "description": "The code change to apply to the file.\nThe system is very smart and can understand how to apply your edits to the files, you just need to provide minimal hints.\nAvoid repeating existing code, instead use comments to represent regions of unchanged code. Be as concise as possible. For example:\n// ...existing code...\n{ changed code }\n// ...existing code...\n{ changed code }\n// ...existing code...\n\nHere is an example of how you should use format an edit to an existing Person class:\nclass Person {\n\t// ...existing code...\n\tage: number;\n\t// ...existing code...\n\tgetAge() {\n\t\treturn this.age;\n\t}\n}" } }, "required": [ "explanation", "filePath", "code" ] } }, { "name": "copilot_createFile", "toolReferenceName": "createFile", "legacyToolReferenceFullNames": [ "createFile" ], "displayName": "%copilot.tools.createFile.name%", "userDescription": "%copilot.tools.createFile.description%", "modelDescription": "This is a tool for creating a new file in the workspace. The file will be created with the specified content. The directory will be created if it does not already exist. Never use this tool to edit a file that already exists.", "tags": [], "inputSchema": { "type": "object", "properties": { "filePath": { "type": "string", "description": "The absolute path to the file to create." }, "content": { "type": "string", "description": "The content to write to the file." } }, "required": [ "filePath", "content" ] } }, { "name": "copilot_createDirectory", "toolReferenceName": "createDirectory", "legacyToolReferenceFullNames": [ "createDirectory" ], "displayName": "%copilot.tools.createDirectory.name%", "userDescription": "%copilot.tools.createDirectory.description%", "modelDescription": "Create a new directory structure in the workspace. Will recursively create all directories in the path, like mkdir -p. You do not need to use this tool before using create_file, that tool will automatically create the needed directories.", "tags": [], "inputSchema": { "type": "object", "properties": { "dirPath": { "type": "string", "description": "The absolute path to the directory to create." } }, "required": [ "dirPath" ] } }, { "name": "copilot_replaceString", "toolReferenceName": "replaceString", "displayName": "%copilot.tools.replaceString.name%", "modelDescription": "This is a tool for making edits in an existing file in the workspace. For moving or renaming files, use run in terminal tool with the 'mv' command instead. For larger edits, split them into smaller edits and call the edit tool multiple times to ensure accuracy. Before editing, always ensure you have the context to understand the file's contents and context. To edit a file, provide: 1) filePath (absolute path), 2) oldString (MUST be the exact literal text to replace including all whitespace, indentation, newlines, and surrounding code etc), and 3) newString (MUST be the exact literal text to replace \\`oldString\\` with (also including all whitespace, indentation, newlines, and surrounding code etc.). Ensure the resulting code is correct and idiomatic.). Each use of this tool replaces exactly ONE occurrence of oldString.\n\nCRITICAL for \\`oldString\\`: Must uniquely identify the single instance to change. Include at least 3 lines of context BEFORE and AFTER the target text, matching whitespace and indentation precisely. If this string matches multiple locations, or does not match exactly, the tool will fail. Never use 'Lines 123-456 omitted' from summarized documents or ...existing code... comments in the oldString or newString.", "when": "!config.github.copilot.chat.disableReplaceTool", "inputSchema": { "type": "object", "properties": { "filePath": { "type": "string", "description": "An absolute path to the file to edit." }, "oldString": { "type": "string", "description": "The exact literal text to replace, preferably unescaped. For single replacements (default), include at least 3 lines of context BEFORE and AFTER the target text, matching whitespace and indentation precisely. For multiple replacements, specify expected_replacements parameter. If this string is not the exact literal text (i.e. you escaped it) or does not match exactly, the tool will fail." }, "newString": { "type": "string", "description": "The exact literal text to replace `old_string` with, preferably unescaped. Provide the EXACT text. Ensure the resulting code is correct and idiomatic." } }, "required": [ "filePath", "oldString", "newString" ] } }, { "name": "copilot_multiReplaceString", "toolReferenceName": "multiReplaceString", "displayName": "%copilot.tools.multiReplaceString.name%", "modelDescription": "This tool allows you to apply multiple replace_string_in_file operations in a single call, which is more efficient than calling replace_string_in_file multiple times. It takes an array of replacement operations and applies them sequentially. Each replacement operation has the same parameters as replace_string_in_file: filePath, oldString, newString, and explanation. This tool is ideal when you need to make multiple edits across different files or multiple edits in the same file. The tool will provide a summary of successful and failed operations.", "when": "!config.github.copilot.chat.disableReplaceTool", "inputSchema": { "type": "object", "properties": { "explanation": { "type": "string", "description": "A brief explanation of what the multi-replace operation will accomplish." }, "replacements": { "type": "array", "description": "An array of replacement operations to apply sequentially.", "items": { "type": "object", "properties": { "filePath": { "type": "string", "description": "An absolute path to the file to edit." }, "oldString": { "type": "string", "description": "The exact literal text to replace, preferably unescaped. Include at least 3 lines of context BEFORE and AFTER the target text, matching whitespace and indentation precisely. If this string is not the exact literal text or does not match exactly, this replacement will fail." }, "newString": { "type": "string", "description": "The exact literal text to replace `oldString` with, preferably unescaped. Provide the EXACT text. Ensure the resulting code is correct and idiomatic." } }, "required": [ "filePath", "oldString", "newString" ] }, "minItems": 1 } }, "required": [ "explanation", "replacements" ] } }, { "name": "copilot_editNotebook", "toolReferenceName": "editNotebook", "icon": "$(pencil)", "displayName": "%copilot.tools.editNotebook.name%", "userDescription": "%copilot.tools.editNotebook.userDescription%", "modelDescription": "This is a tool for editing an existing Notebook file in the workspace. Generate the \"explanation\" property first.\nThe system is very smart and can understand how to apply your edits to the notebooks.\nWhen updating the content of an existing cell, ensure newCode preserves whitespace and indentation exactly and does NOT include any code markers such as (...existing code...).", "tags": [ "enable_other_tool_copilot_getNotebookSummary" ], "inputSchema": { "type": "object", "properties": { "filePath": { "type": "string", "description": "An absolute path to the notebook file to edit, or the URI of a untitled, not yet named, file, such as `untitled:Untitled-1." }, "cellId": { "type": "string", "description": "Id of the cell that needs to be deleted or edited. Use the value `TOP`, `BOTTOM` when inserting a cell at the top or bottom of the notebook, else provide the id of the cell after which a new cell is to be inserted. Remember, if a cellId is provided and editType=insert, then a cell will be inserted after the cell with the provided cellId." }, "newCode": { "anyOf": [ { "type": "string", "description": "The code for the new or existing cell to be edited. Code should not be wrapped within tags. Do NOT include code markers such as (...existing code...) to indicate existing code." }, { "type": "array", "items": { "type": "string", "description": "The code for the new or existing cell to be edited. Code should not be wrapped within tags" } } ] }, "language": { "type": "string", "description": "The language of the cell. `markdown`, `python`, `javascript`, `julia`, etc." }, "editType": { "type": "string", "enum": [ "insert", "delete", "edit" ], "description": "The operation peformed on the cell, whether `insert`, `delete` or `edit`.\nUse the `editType` field to specify the operation: `insert` to add a new cell, `edit` to modify an existing cell's content, and `delete` to remove a cell." } }, "required": [ "filePath", "editType", "cellId" ] } }, { "name": "copilot_runNotebookCell", "displayName": "%copilot.tools.runNotebookCell.name%", "toolReferenceName": "runNotebookCell", "legacyToolReferenceFullNames": [ "runNotebooks/runCell" ], "icon": "$(play)", "modelDescription": "This is a tool for running a code cell in a notebook file directly in the notebook editor. The output from the execution will be returned. Code cells should be run as they are added or edited when working through a problem to bring the kernel state up to date and ensure the code executes successfully. Code cells are ready to run and don't require any pre-processing. If asked to run the first cell in a notebook, you should run the first code cell since markdown cells cannot be executed. NOTE: Avoid executing Markdown cells or providing Markdown cell IDs, as Markdown cells cannot be executed.", "userDescription": "%copilot.tools.runNotebookCell.description%", "tags": [ "enable_other_tool_copilot_getNotebookSummary" ], "inputSchema": { "type": "object", "properties": { "filePath": { "type": "string", "description": "An absolute path to the notebook file with the cell to run, or the URI of a untitled, not yet named, file, such as `untitled:Untitled-1.ipynb" }, "reason": { "type": "string", "description": "An optional explanation of why the cell is being run. This will be shown to the user before the tool is run and is not necessary if it's self-explanatory." }, "cellId": { "type": "string", "description": "The ID for the code cell to execute. Avoid providing markdown cell IDs as nothing will be executed." }, "continueOnError": { "type": "boolean", "description": "Whether or not execution should continue for remaining cells if an error is encountered. Default to false unless instructed otherwise." } }, "required": [ "filePath", "cellId" ] } }, { "name": "copilot_getNotebookSummary", "toolReferenceName": "getNotebookSummary", "legacyToolReferenceFullNames": [ "runNotebooks/getNotebookSummary" ], "displayName": "Get the structure of a notebook", "modelDescription": "This is a tool returns the list of the Notebook cells along with the id, cell types, line ranges, language, execution information and output mime types for each cell. This is useful to get Cell Ids when executing a notebook or determine what cells have been executed and what order, or what cells have outputs. If required to read contents of a cell use this to determine the line range of a cells, and then use read_file tool to read a specific line range. Requery this tool if the contents of the notebook change.", "tags": [], "inputSchema": { "type": "object", "properties": { "filePath": { "type": "string", "description": "An absolute path to the notebook file with the cell to run, or the URI of a untitled, not yet named, file, such as `untitled:Untitled-1.ipynb" } }, "required": [ "filePath" ] } }, { "name": "copilot_readNotebookCellOutput", "displayName": "%copilot.tools.getNotebookCellOutput.name%", "toolReferenceName": "readNotebookCellOutput", "legacyToolReferenceFullNames": [ "runNotebooks/readNotebookCellOutput" ], "icon": "$(notebook-render-output)", "modelDescription": "This tool will retrieve the output for a notebook cell from its most recent execution or restored from disk. The cell may have output even when it has not been run in the current kernel session. This tool has a higher token limit for output length than the runNotebookCell tool.", "userDescription": "%copilot.tools.getNotebookCellOutput.description%", "when": "userHasOpenedNotebook", "tags": [], "inputSchema": { "type": "object", "properties": { "filePath": { "type": "string", "description": "An absolute path to the notebook file with the cell to run, or the URI of a untitled, not yet named, file, such as `untitled:Untitled-1.ipynb" }, "cellId": { "type": "string", "description": "The ID of the cell for which output should be retrieved." } }, "required": [ "filePath", "cellId" ] } }, { "name": "copilot_fetchWebPage", "displayName": "%copilot.tools.fetchWebPage.name%", "toolReferenceName": "fetch", "legacyToolReferenceFullNames": [ "fetch" ], "when": "!isWeb", "icon": "$(globe)", "userDescription": "%copilot.tools.fetchWebPage.description%", "modelDescription": "Fetches the main content from a web page. This tool is useful for summarizing or analyzing the content of a webpage. You should use this tool when you think the user is looking for information from a specific webpage.", "tags": [], "inputSchema": { "type": "object", "properties": { "urls": { "type": "array", "items": { "type": "string" }, "description": "An array of URLs to fetch content from." }, "query": { "type": "string", "description": "The query to search for in the web page's content. This should be a clear and concise description of the content you want to find." } }, "required": [ "urls", "query" ] } }, { "name": "copilot_findTestFiles", "displayName": "%copilot.tools.findTestFiles.name%", "icon": "$(beaker)", "canBeReferencedInPrompt": false, "toolReferenceName": "findTestFiles", "userDescription": "%copilot.tools.findTestFiles.description%", "modelDescription": "For a source code file, find the file that contains the tests. For a test file find the file that contains the code under test.", "tags": [], "inputSchema": { "type": "object", "properties": { "filePaths": { "type": "array", "items": { "type": "string" } } }, "required": [ "filePaths" ] } }, { "name": "copilot_getSearchResults", "toolReferenceName": "searchResults", "displayName": "%github.copilot.tools.searchResults.name%", "icon": "$(search)", "userDescription": "%github.copilot.tools.searchResults.description%", "modelDescription": "The results from the search view", "when": "!config.github.copilot.chat.getSearchViewResultsSkill.enabled" }, { "name": "copilot_githubRepo", "toolReferenceName": "githubRepo", "legacyToolReferenceFullNames": [ "githubRepo" ], "displayName": "%github.copilot.tools.githubRepo.name%", "modelDescription": "Searches a GitHub repository for relevant source code snippets. Only use this tool if the user is very clearly asking for code snippets from a specific GitHub repository. Do not use this tool for Github repos that the user has open in their workspace.", "userDescription": "%github.copilot.tools.githubRepo.userDescription%", "icon": "$(repo)", "when": "!config.github.copilot.chat.githubMcpServer.enabled", "inputSchema": { "type": "object", "properties": { "repo": { "type": "string", "description": "The name of the Github repository to search for code in. Should must be formatted as '/'." }, "query": { "type": "string", "description": "The query to search for repo. Should contain all relevant context." } }, "required": [ "repo", "query" ] } }, { "name": "copilot_toolReplay", "modelDescription": "Replays a tool call from a previous chat session.", "displayName": "tool replay", "when": "false", "inputSchema": { "type": "object", "properties": { "toolCallId": { "type": "string", "description": "the id of the tool original tool call" }, "toolName": { "type": "string", "description": "the name of the tool being replayed" }, "toolCallArgs": { "type": "object", "description": "the arguments of the tool call" } } } }, { "name": "copilot_switchAgent", "toolReferenceName": "switchAgent", "displayName": "%copilot.tools.switchAgent.name%", "userDescription": "%copilot.tools.switchAgent.description%", "modelDescription": "Switch to the Plan agent to align on approach before implementing. Plan will explore the codebase, gathers context, clarifies requirements with the user, and creates an actionable implementation plan.\n\nSWITCH TO PLAN when ANY of these apply:\n1. Adding new functionality - where should it go? What patterns to follow?\n2. Multiple valid approaches exist - choosing between technologies, patterns, or strategies\n3. Modifying existing behavior - unclear what should change or what side effects exist\n4. Architectural decisions required - choosing between design patterns or integration approaches\n5. Changes span multiple files - refactoring, migrations, or cross-cutting concerns\n6. Requirements are underspecified - need to explore before understanding scope\n\nEXAMPLES:\n✓ Switch to Plan:\n- \"Add authentication to the app\" → architectural decisions needed (session vs JWT, middleware)\n- \"Refactor this data flow\" → must understand component dependencies first\n- \"Migrate from X to Y\" → requires understanding current structure\n\n✗ Do NOT switch to Plan:\n- User attached a detailed spec, plan, or requirements doc → context already provided\n- You already started editing files in this conversation → too late to switch\n- Single obvious change like fixing a typo or renaming → just do it\n- User gave explicit step-by-step instructions → follow them directly", "when": "config.github.copilot.chat.switchAgent.enabled", "icon": "$(arrow-swap)", "inputSchema": { "type": "object", "properties": { "agentName": { "type": "string", "description": "The name of the agent to switch to. Currently only 'Plan' is supported.", "enum": [ "Plan" ] } }, "required": [ "agentName" ] } }, { "name": "copilot_memory", "displayName": "Memory", "toolReferenceName": "memory", "userDescription": "Manage persistent memory across conversations", "when": "config.github.copilot.chat.tools.memory.enabled", "modelDescription": "Manage a persistent memory system with three scopes for storing notes and information across conversations.\n\nMemory is organized under /memories/ with three tiers:\n- `/memories/` — User memory: persistent notes that survive across all workspaces and conversations. Store preferences, patterns, and general insights here.\n- `/memories/session/` — Session memory: notes scoped to the current conversation. Store task-specific context and in-progress notes here. Cleared after the conversation ends.\n- `/memories/repo/` — Repository memory: repository-scoped facts stored via Copilot. Only the `create` command is supported for this path.\n\nIMPORTANT: Before creating new memory files, first view the /memories/ directory to understand what already exists. This helps avoid duplicates and maintain organized notes.\n\nCommands:\n- `view`: View contents of a file or list directory contents. Can be used on files or directories (e.g., \"/memories/\" to see all top-level items).\n- `create`: Create a new file at the specified path with the given content. Fails if the file already exists.\n- `str_replace`: Replace an exact string in a file with a new string. The old_str must appear exactly once in the file.\n- `insert`: Insert text at a specific line number in a file. Line 0 inserts at the beginning.\n- `delete`: Delete a file or directory (and all its contents).\n- `rename`: Rename or move a file or directory from path to new_path. Cannot rename across scopes.", "inputSchema": { "type": "object", "properties": { "command": { "type": "string", "enum": [ "view", "create", "str_replace", "insert", "delete", "rename" ], "description": "The operation to perform on the memory file system." }, "path": { "type": "string", "description": "The absolute path to the file or directory inside /memories/, e.g. \"/memories/notes.md\". Used by all commands except `rename`." }, "file_text": { "type": "string", "description": "Required for `create`. The content of the file to create." }, "old_str": { "type": "string", "description": "Required for `str_replace`. The exact string in the file to replace. Must appear exactly once." }, "new_str": { "type": "string", "description": "Required for `str_replace`. The new string to replace old_str with." }, "insert_line": { "type": "number", "description": "Required for `insert`. The 0-based line number to insert text at. 0 inserts before the first line." }, "insert_text": { "type": "string", "description": "Required for `insert`. The text to insert at the specified line." }, "view_range": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2, "description": "Optional for `view`. A two-element array [start_line, end_line] (1-indexed) to view a specific range of lines." }, "old_path": { "type": "string", "description": "Required for `rename`. The current path of the file or directory to rename." }, "new_path": { "type": "string", "description": "Required for `rename`. The new path for the file or directory." } }, "required": [ "command" ] } }, { "name": "copilot_resolveMemoryFileUri", "displayName": "Resolve Memory File URI", "toolReferenceName": "resolveMemoryFileUri", "userDescription": "Resolve a memory file path to its actual URI", "modelDescription": "Resolve a memory file path (like /memories/session/plan.md or /memories/repo/notes.md) to its fully qualified URI. Use this when you need the actual URI for a memory file, for example to pass it to setArtifacts. The path must start with /memories/.", "tags": [], "inputSchema": { "type": "object", "properties": { "path": { "type": "string", "description": "The memory file path to resolve (e.g. /memories/session/plan.md)." } }, "required": [ "path" ] } }, { "name": "copilot_editFiles", "modelDescription": "This is a placeholder tool, do not use", "userDescription": "Edit files", "icon": "$(pencil)", "displayName": "Edit Files", "toolReferenceName": "editFiles", "legacyToolReferenceFullNames": [ "editFiles" ] } ], "languageModelToolSets": [ { "name": "edit", "description": "%copilot.toolSet.editing.description%", "icon": "$(pencil)", "tools": [ "createDirectory", "createFile", "createJupyterNotebook", "editFiles", "editNotebook", "rename" ] }, { "name": "execute", "description": "", "tools": [ "runNotebookCell", "testFailure" ] }, { "name": "read", "description": "%copilot.toolSet.read.description%", "icon": "$(eye)", "tools": [ "getNotebookSummary", "problems", "readFile", "viewImage", "readNotebookCellOutput" ] }, { "name": "search", "description": "%copilot.toolSet.search.description%", "icon": "$(search)", "tools": [ "changes", "codebase", "fileSearch", "listDirectory", "searchResults", "textSearch", "searchSubagent", "usages" ] }, { "name": "vscode", "description": "", "tools": [ "getProjectSetupInfo", "installExtension", "memory", "newWorkspace", "resolveMemoryFileUri", "runCommand", "switchAgent", "vscodeAPI" ] }, { "name": "web", "description": "%copilot.toolSet.web.description%", "icon": "$(globe)", "tools": [ "fetch", "githubRepo" ] } ], "chatParticipants": [ { "id": "github.copilot.default", "name": "GitHubCopilot", "fullName": "GitHub Copilot", "description": "%copilot.description%", "isDefault": true, "locations": [ "panel" ], "modes": [ "ask" ], "disambiguation": [ { "category": "generate_code_sample", "description": "The user wants to generate code snippets without referencing the contents of the current workspace. This category does not include generating entire projects.", "examples": [ "Write an example of computing a SHA256 hash." ] }, { "category": "add_feature_to_file", "description": "The user wants to change code in a file that is provided in their request, without referencing the contents of the current workspace. This category does not include generating entire projects.", "examples": [ "Add a refresh button to the table widget." ] }, { "category": "question_about_specific_files", "description": "The user has a question about a specific file or code snippet that they have provided as part of their query, and the question does not require additional workspace context to answer.", "examples": [ "What does this file do?" ] } ], "commands": [ { "name": "explain", "description": "%copilot.workspace.explain.description%" }, { "name": "review", "description": "%copilot.workspace.review.description%", "when": "github.copilot.advanced.review.intent" }, { "name": "tests", "description": "%copilot.workspace.tests.description%", "disambiguation": [ { "category": "create_tests", "description": "The user wants to generate unit tests.", "examples": [ "Generate tests for my selection using pytest." ] } ] }, { "name": "fix", "description": "%copilot.workspace.fix.description%", "sampleRequest": "%copilot.workspace.fix.sampleRequest%" }, { "name": "new", "description": "%copilot.workspace.new.description%", "sampleRequest": "%copilot.workspace.new.sampleRequest%", "isSticky": true, "disambiguation": [ { "category": "create_new_workspace_or_extension", "description": "The user wants to create a complete Visual Studio Code workspace from scratch, such as a new application or a Visual Studio Code extension. Use this category only if the question relates to generating or creating new workspaces in Visual Studio Code. Do not use this category for updating existing code or generating sample code snippets", "examples": [ "Scaffold a Node server.", "Create a sample project which uses the fileSystemProvider API.", "react application" ] } ] }, { "name": "newNotebook", "description": "%copilot.workspace.newNotebook.description%", "sampleRequest": "%copilot.workspace.newNotebook.sampleRequest%", "disambiguation": [ { "category": "create_jupyter_notebook", "description": "The user wants to create a new Jupyter notebook in Visual Studio Code.", "examples": [ "Create a notebook to analyze this CSV file." ] } ] }, { "name": "semanticSearch", "description": "%copilot.workspace.semanticSearch.description%", "sampleRequest": "%copilot.workspace.semanticSearch.sampleRequest%", "when": "config.github.copilot.semanticSearch.enabled" }, { "name": "setupTests", "description": "%copilot.vscode.setupTests.description%", "sampleRequest": "%copilot.vscode.setupTests.sampleRequest%", "when": "config.github.copilot.chat.setupTests.enabled", "disambiguation": [ { "category": "set_up_tests", "description": "The user wants to configure project test setup, framework, or test runner. The user does not want to fix their existing tests.", "examples": [ "Set up tests for this project." ] } ] } ] }, { "id": "github.copilot.editingSession", "name": "GitHubCopilot", "fullName": "GitHub Copilot", "description": "%copilot.edits.description%", "isDefault": true, "locations": [ "panel" ], "modes": [ "edit" ] }, { "id": "github.copilot.editingSessionEditor", "name": "GitHubCopilot", "fullName": "GitHub Copilot", "description": "%copilot.edits.description%", "isDefault": true, "locations": [ "editor" ], "commands": [ { "name": "generate", "when": "!config.inlineChat.enableV2", "description": "%copilot.workspace.generate.description%", "disambiguation": [ { "category": "generate", "description": "Generate new code", "examples": [ "Add a function that returns the sum of two numbers" ] } ] }, { "name": "edit", "when": "!config.inlineChat.enableV2", "description": "%copilot.workspace.edit.inline.description%", "disambiguation": [ { "category": "edit", "description": "Make changes to existing code", "examples": [ "Change this method to use async/await" ] } ] }, { "name": "doc", "when": "!config.inlineChat.enableV2", "description": "%copilot.workspace.doc.description%", "disambiguation": [ { "category": "doc", "description": "Add documentation comment for this symbol", "examples": [ "Add jsdoc to this method" ] } ] }, { "name": "fix", "when": "!config.inlineChat.enableV2", "description": "%copilot.workspace.fix.description%", "disambiguation": [ { "category": "fix", "description": "Propose a fix for the problems in the selected code", "examples": [ "There is a problem in this code. Rewrite the code to show it with the bug fixed." ] } ] }, { "name": "tests", "when": "!config.inlineChat.enableV2", "description": "%copilot.workspace.tests.description%", "disambiguation": [ { "category": "tests", "description": "Generate unit tests for the selected code. The user does not want to fix their existing tests.", "examples": [ "Write a set of detailed unit test functions for the code above." ] } ] } ] }, { "id": "github.copilot.editsAgent", "name": "agent", "fullName": "GitHub Copilot", "description": "%copilot.agent.description%", "locations": [ "panel" ], "modes": [ "agent" ], "isEngine": true, "isDefault": true, "isAgent": true, "when": "config.chat.agent.enabled", "commands": [ { "name": "error", "description": "Make a model request which will result in an error", "when": "github.copilot.chat.debug" }, { "name": "compact", "description": "%copilot.agent.compact.description%" }, { "name": "explain", "description": "%copilot.workspace.explain.description%" }, { "name": "review", "description": "%copilot.workspace.review.description%", "when": "github.copilot.advanced.review.intent" }, { "name": "tests", "description": "%copilot.workspace.tests.description%", "disambiguation": [ { "category": "create_tests", "description": "The user wants to generate unit tests.", "examples": [ "Generate tests for my selection using pytest." ] } ] }, { "name": "fix", "description": "%copilot.workspace.fix.description%", "sampleRequest": "%copilot.workspace.fix.sampleRequest%" }, { "name": "new", "description": "%copilot.workspace.new.description%", "sampleRequest": "%copilot.workspace.new.sampleRequest%", "isSticky": true, "disambiguation": [ { "category": "create_new_workspace_or_extension", "description": "The user wants to create a complete Visual Studio Code workspace from scratch, such as a new application or a Visual Studio Code extension. Use this category only if the question relates to generating or creating new workspaces in Visual Studio Code. Do not use this category for updating existing code or generating sample code snippets", "examples": [ "Scaffold a Node server.", "Create a sample project which uses the fileSystemProvider API.", "react application" ] } ] }, { "name": "newNotebook", "description": "%copilot.workspace.newNotebook.description%", "sampleRequest": "%copilot.workspace.newNotebook.sampleRequest%", "disambiguation": [ { "category": "create_jupyter_notebook", "description": "The user wants to create a new Jupyter notebook in Visual Studio Code.", "examples": [ "Create a notebook to analyze this CSV file." ] } ] }, { "name": "semanticSearch", "description": "%copilot.workspace.semanticSearch.description%", "sampleRequest": "%copilot.workspace.semanticSearch.sampleRequest%", "when": "config.github.copilot.semanticSearch.enabled" }, { "name": "setupTests", "description": "%copilot.vscode.setupTests.description%", "sampleRequest": "%copilot.vscode.setupTests.sampleRequest%", "when": "config.github.copilot.chat.setupTests.enabled", "disambiguation": [ { "category": "set_up_tests", "description": "The user wants to configure project test setup, framework, or test runner. The user does not want to fix their existing tests.", "examples": [ "Set up tests for this project." ] } ] } ] }, { "id": "github.copilot.notebook", "name": "GitHubCopilot", "fullName": "GitHub Copilot", "description": "%copilot.description%", "isDefault": true, "locations": [ "notebook" ], "when": "!config.inlineChat.notebookAgent", "commands": [ { "name": "fix", "description": "%copilot.workspace.fix.description%" }, { "name": "explain", "description": "%copilot.workspace.explain.description%" } ] }, { "id": "github.copilot.notebookEditorAgent", "name": "GitHubCopilot", "fullName": "GitHub Copilot", "description": "%copilot.description%", "isDefault": true, "locations": [ "notebook" ], "when": "config.inlineChat.notebookAgent", "commands": [ { "name": "fix", "description": "%copilot.workspace.fix.description%" }, { "name": "explain", "description": "%copilot.workspace.explain.description%" } ] }, { "id": "github.copilot.vscode", "name": "vscode", "fullName": "VS Code", "description": "%copilot.vscode.description%", "when": "!github.copilot.interactiveSession.disabled", "sampleRequest": "%copilot.vscode.sampleRequest%", "locations": [ "panel" ], "disambiguation": [ { "category": "vscode_configuration_questions", "description": "The user wants to learn about, use, or configure the Visual Studio Code. Use this category if the users question is specifically about commands, settings, keybindings, extensions and other features available in Visual Studio Code. Do not use this category to answer questions about generating code or creating new projects including Visual Studio Code extensions.", "examples": [ "Switch to light mode.", "Keyboard shortcut to toggle terminal visibility.", "Settings to enable minimap.", "Whats new in the latest release?" ] }, { "category": "configure_python_environment", "description": "The user wants to set up their Python environment.", "examples": [ "Create a virtual environment for my project." ] } ], "commands": [ { "name": "search", "description": "%copilot.vscode.search.description%", "sampleRequest": "%copilot.vscode.search.sampleRequest%" } ] }, { "id": "github.copilot.terminal", "name": "terminal", "fullName": "Terminal", "description": "%copilot.terminal.description%", "when": "!github.copilot.interactiveSession.disabled", "sampleRequest": "%copilot.terminal.sampleRequest%", "isDefault": true, "locations": [ "terminal" ], "commands": [ { "name": "explain", "description": "%copilot.terminal.explain.description%", "sampleRequest": "%copilot.terminal.explain.sampleRequest%" } ] }, { "id": "github.copilot.terminalPanel", "name": "terminal", "fullName": "Terminal", "description": "%copilot.terminalPanel.description%", "when": "!github.copilot.interactiveSession.disabled", "sampleRequest": "%copilot.terminal.sampleRequest%", "locations": [ "panel" ], "commands": [ { "name": "explain", "description": "%copilot.terminal.explain.description%", "sampleRequest": "%copilot.terminal.explain.sampleRequest%", "disambiguation": [ { "category": "terminal_state_questions", "description": "The user wants to learn about specific state such as the selection, command, or failed command in the integrated terminal in Visual Studio Code.", "examples": [ "Why did the latest terminal command fail?" ] } ] } ] }, { "id": "github.copilot.chatReplay", "name": "chatReplay", "fullName": "Chat Replay", "when": "debugType == 'vscode-chat-replay'", "locations": [ "panel" ] } ], "languageModelChatProviders": [ { "vendor": "copilot", "displayName": "Copilot" }, { "vendor": "copilotcli", "displayName": "Copilot CLI", "when": "false" }, { "vendor": "claude-code", "displayName": "Claude Code", "when": "false" }, { "vendor": "anthropic", "displayName": "Anthropic", "configuration": { "properties": { "apiKey": { "type": "string", "secret": true, "description": "API key for Anthropic", "title": "API Key" } }, "required": [ "apiKey" ] } }, { "vendor": "xai", "displayName": "xAI", "configuration": { "properties": { "apiKey": { "type": "string", "secret": true, "description": "API key for xAI", "title": "API Key" } }, "required": [ "apiKey" ] } }, { "vendor": "gemini", "displayName": "Google", "configuration": { "properties": { "apiKey": { "type": "string", "secret": true, "description": "API key for Google Gemini", "title": "API Key" } }, "required": [ "apiKey" ] } }, { "vendor": "openrouter", "displayName": "OpenRouter", "configuration": { "properties": { "apiKey": { "type": "string", "secret": true, "description": "API key for OpenRouter", "title": "API Key" } }, "required": [ "apiKey" ] } }, { "vendor": "openai", "displayName": "OpenAI", "configuration": { "properties": { "apiKey": { "type": "string", "secret": true, "description": "API key for OpenAI", "title": "API Key" } }, "required": [ "apiKey" ] } }, { "vendor": "ollama", "displayName": "Ollama", "configuration": { "type": "object", "properties": { "url": { "type": "string", "description": "The endpoint URL for the Ollama server", "default": "http://localhost:11434", "title": "URL" } }, "required": [ "url" ] } }, { "vendor": "customoai", "when": "productQualityType != 'stable'", "displayName": "OpenAI Compatible", "configuration": { "type": "object", "properties": { "apiKey": { "type": "string", "secret": true, "description": "API key for the models", "title": "API Key" }, "models": { "type": "array", "defaultSnippets": [ { "label": "New Model", "description": "Add a new custom model configuration", "body": [ { "id": "$1", "name": "$2", "url": "$3", "toolCalling": "^${4|true,false|}", "vision": "^${5|true,false|}", "maxInputTokens": "^${6:128000}", "maxOutputTokens": "^${7:16000}" } ] } ], "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the model" }, "name": { "type": "string", "description": "Display name of the custom OpenAI model" }, "url": { "type": "string", "markdownDescription": "URL endpoint for the custom OpenAI-compatible model.\n\n**Important:** Base URLs default to Chat Completions API. Explicit API paths including `/responses` or `/chat/completions` are respected." }, "toolCalling": { "type": "boolean", "description": "Whether the model supports tool calling" }, "vision": { "type": "boolean", "description": "Whether the model supports vision capabilities" }, "maxInputTokens": { "type": "number", "description": "Maximum number of input tokens supported by the model" }, "maxOutputTokens": { "type": "number", "description": "Maximum number of output tokens supported by the model" }, "editTools": { "type": "array", "description": "List of edit tools supported by the model. If this is not configured, the editor will try multiple edit tools and pick the best one.\n\n- 'find-replace': Find and replace text in a document.\n- 'multi-find-replace': Find and replace text in a document.\n- 'apply-patch': A file-oriented diff format used by some OpenAI models\n- 'code-rewrite': A general but slower editing tool that allows the model to rewrite and code snippet and provide only the replacement to the editor.", "items": { "type": "string", "enum": [ "find-replace", "multi-find-replace", "apply-patch", "code-rewrite" ] } }, "thinking": { "type": "boolean", "default": false, "description": "Whether the model supports thinking capabilities" }, "streaming": { "type": "boolean", "default": true, "description": "Whether the model supports streaming responses. Defaults to true." }, "zeroDataRetentionEnabled": { "type": "boolean", "default": false, "markdownDescription": "Whether Zero Data Retention (ZDR) is enabled for this endpoint. When `true`, `previous_response_id` will not be sent in requests via Responses API." }, "requestHeaders": { "type": "object", "description": "Additional HTTP headers to include with requests to this model. These reserved headers are not allowed and ignored if present: forbidden request headers (https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_request_header), forwarding headers ('forwarded', 'x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto'), and others ('api-key', 'authorization', 'content-type', 'openai-intent', 'x-github-api-version', 'x-initiator', 'x-interaction-id', 'x-interaction-type', 'x-onbehalf-extension-id', 'x-request-id', 'x-vscode-user-agent-library-version'). Pattern-based forbidden headers ('proxy-*', 'sec-*', 'x-http-method*' with forbidden methods) are also blocked.", "additionalProperties": { "type": "string" } } }, "required": [ "id", "name", "url", "toolCalling", "vision", "maxInputTokens", "maxOutputTokens" ] } } } } }, { "vendor": "azure", "displayName": "Azure", "configuration": { "type": "object", "properties": { "apiKey": { "type": "string", "secret": true, "description": "API key for the models. If not set then Entra ID (Azure AD) authentication with your Microsoft account credentials will be used.", "title": "API Key" }, "models": { "type": "array", "defaultSnippets": [ { "label": "New Model", "description": "Add a new custom model configuration", "body": [ { "id": "$1", "name": "$2", "url": "$3", "toolCalling": "^${4|true,false|}", "vision": "^${5|true,false|}", "maxInputTokens": "^${6:128000}", "maxOutputTokens": "^${7:16000}" } ] } ], "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the model" }, "name": { "type": "string", "description": "Display name of the custom OpenAI model" }, "url": { "type": "string", "markdownDescription": "URL endpoint for the custom OpenAI-compatible model.\n\n**Important:** Base URLs default to Chat Completions API. Explicit API paths including `/responses` or `/chat/completions` are respected." }, "toolCalling": { "type": "boolean", "description": "Whether the model supports tool calling" }, "vision": { "type": "boolean", "description": "Whether the model supports vision capabilities" }, "maxInputTokens": { "type": "number", "description": "Maximum number of input tokens supported by the model" }, "maxOutputTokens": { "type": "number", "description": "Maximum number of output tokens supported by the model" }, "thinking": { "type": "boolean", "default": false, "description": "Whether the model supports thinking capabilities" }, "streaming": { "type": "boolean", "default": true, "description": "Whether the model supports streaming responses. Defaults to true." }, "zeroDataRetentionEnabled": { "type": "boolean", "default": false, "markdownDescription": "Whether Zero Data Retention (ZDR) is enabled for this endpoint. When `true`, `previous_response_id` will not be sent in requests via Responses API." }, "requestHeaders": { "type": "object", "description": "Additional HTTP headers to include with requests to this model. These reserved headers are not allowed and ignored if present: forbidden request headers (https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_request_header), forwarding headers ('forwarded', 'x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto'), and others ('api-key', 'authorization', 'content-type', 'openai-intent', 'x-github-api-version', 'x-initiator', 'x-interaction-id', 'x-interaction-type', 'x-onbehalf-extension-id', 'x-request-id', 'x-vscode-user-agent-library-version'). Pattern-based forbidden headers ('proxy-*', 'sec-*', 'x-http-method*' with forbidden methods) are also blocked.", "additionalProperties": { "type": "string" } } }, "required": [ "id", "name", "url", "toolCalling", "vision", "maxInputTokens", "maxOutputTokens" ] } } } } } ], "interactiveSession": [ { "label": "GitHub Copilot", "id": "copilot", "icon": "", "when": "!github.copilot.interactiveSession.disabled" } ], "mcpServerDefinitionProviders": [ { "id": "github", "label": "GitHub" } ], "viewsWelcome": [ { "view": "debug", "when": "github.copilot-chat.activated", "contents": "%github.copilot.viewsWelcome.debug%" } ], "chatViewsWelcome": [ { "icon": "$(chat-sparkle)", "title": "%copilot.title%", "content": "%github.copilot.viewsWelcome.individual.expired%", "when": "github.copilot.interactiveSession.individual.expired" }, { "icon": "$(chat-sparkle)", "title": "%copilot.title%", "content": "%github.copilot.viewsWelcome.enterprise%", "when": "github.copilot.interactiveSession.enterprise.disabled" }, { "icon": "$(chat-sparkle)", "title": "%copilot.title%", "content": "%github.copilot.viewsWelcome.offline%", "when": "github.copilot.offline" }, { "icon": "$(chat-sparkle)", "title": "%copilot.title%", "content": "%github.copilot.viewsWelcome.invalidToken%", "when": "github.copilot.interactiveSession.invalidToken" }, { "icon": "$(chat-sparkle)", "title": "%copilot.title%", "content": "%github.copilot.viewsWelcome.rateLimited%", "when": "github.copilot.interactiveSession.rateLimited" }, { "icon": "$(chat-sparkle)", "title": "%copilot.title%", "content": "%github.copilot.viewsWelcome.gitHubLoginFailed%", "when": "github.copilot.interactiveSession.gitHubLoginFailed" }, { "icon": "$(chat-sparkle)", "title": "%copilot.title%", "content": "%github.copilot.viewsWelcome.contactSupport%", "when": "github.copilot.interactiveSession.contactSupport" }, { "icon": "$(chat-sparkle)", "title": "%copilot.title%", "content": "%github.copilot.viewsWelcome.chatDisabled%", "when": "github.copilot.interactiveSession.chatDisabled" }, { "icon": "$(chat-sparkle)", "title": "%copilot.title%", "content": "%github.copilot.viewsWelcome.switchToReleaseChannel%", "when": "github.copilot.interactiveSession.switchToReleaseChannel" } ], "commands": [ { "command": "github.copilot.chat.triggerPermissiveSignIn", "title": "%github.copilot.command.triggerPermissiveSignIn%" }, { "command": "copilot.claude.agents", "title": "Manage Agents", "category": "Claude Agent" }, { "command": "copilot.claude.hooks", "title": "Configure Hooks", "category": "Claude Agent" }, { "command": "copilot.claude.memory", "title": "Open Memory Files", "category": "Claude Agent" }, { "command": "github.copilot.cli.sessions.delete", "title": "%github.copilot.command.deleteAgentSession%", "icon": "$(close)", "category": "Copilot CLI" }, { "command": "github.copilot.cli.sessions.resumeInTerminal", "title": "%github.copilot.command.cli.sessions.resumeInTerminal%", "icon": "$(terminal)", "category": "Copilot CLI" }, { "command": "github.copilot.cli.sessions.rename", "title": "%github.copilot.command.cli.sessions.rename%", "icon": "$(edit)", "category": "Copilot CLI" }, { "command": "github.copilot.claude.sessions.rename", "title": "%github.copilot.command.claude.sessions.rename%", "icon": "$(edit)", "category": "Claude" }, { "command": "github.copilot.cli.sessions.openRepository", "title": "%github.copilot.command.cli.sessions.openRepository%", "icon": "$(folder-opened)", "category": "Copilot CLI" }, { "command": "github.copilot.cli.sessions.openWorktreeInNewWindow", "title": "%github.copilot.command.cli.sessions.openWorktreeInNewWindow%", "icon": "$(folder-opened)", "category": "Copilot CLI" }, { "command": "github.copilot.cli.sessions.openWorktreeInTerminal", "title": "%github.copilot.command.cli.sessions.openWorktreeInTerminal%", "icon": "$(terminal)", "category": "Copilot CLI" }, { "command": "github.copilot.cli.sessions.copyWorktreeBranchName", "title": "%github.copilot.command.cli.sessions.copyWorktreeBranchName%", "icon": "$(copy)", "category": "Copilot CLI" }, { "command": "github.copilot.cli.sessions.commitToWorktree", "title": "%github.copilot.command.cli.sessions.commitToWorktree%", "icon": "$(git-commit)", "category": "Copilot CLI" }, { "command": "github.copilot.cli.sessions.commitToRepository", "title": "%github.copilot.command.cli.sessions.commitToRepository%", "icon": "$(git-commit)", "category": "Copilot CLI" }, { "command": "github.copilot.cli.newSession", "title": "%github.copilot.command.cli.newSession%", "icon": "$(terminal)", "category": "Chat" }, { "command": "github.copilot.cli.newSessionToSide", "title": "%github.copilot.command.cli.newSessionToSide%", "icon": "$(terminal)", "category": "Chat" }, { "command": "github.copilot.cli.openInCopilotCLI", "title": "%github.copilot.command.cli.openInCopilotCLI%", "icon": "$(terminal)", "category": "Copilot CLI" }, { "command": "github.copilot.chat.replay", "title": "Start Chat Replay", "icon": "$(debug-line-by-line)", "enablement": "resourceFilename === 'benchRun.chatReplay.json' && !inDebugMode" }, { "command": "github.copilot.chat.replay.enableWorkspaceEditTracing", "title": "%github.copilot.command.enableEditTracing%", "category": "Developer", "enablement": "!github.copilot.chat.replay.workspaceEditTracing" }, { "command": "github.copilot.chat.replay.disableWorkspaceEditTracing", "title": "%github.copilot.command.disableEditTracing%", "category": "Developer", "enablement": "github.copilot.chat.replay.workspaceEditTracing" }, { "command": "github.copilot.chat.compact", "title": "%github.copilot.command.compactConversation%" }, { "command": "github.copilot.chat.explain", "title": "%github.copilot.command.explainThis%", "enablement": "!github.copilot.interactiveSession.disabled", "category": "Chat" }, { "command": "github.copilot.chat.explain.palette", "title": "%github.copilot.command.explainThis%", "enablement": "!github.copilot.interactiveSession.disabled && !editorReadonly", "category": "Chat" }, { "command": "github.copilot.chat.review", "title": "%github.copilot.command.reviewAndComment%", "enablement": "config.github.copilot.chat.reviewSelection.enabled && !github.copilot.interactiveSession.disabled", "category": "Chat" }, { "command": "github.copilot.chat.review.apply", "title": "%github.copilot.command.applyReviewSuggestion%", "icon": "$(sparkle)", "enablement": "commentThread =~ /hasSuggestion/", "category": "Chat" }, { "command": "github.copilot.chat.review.applyAndNext", "title": "%github.copilot.command.applyReviewSuggestionAndNext%", "icon": "$(sparkle)", "enablement": "commentThread =~ /hasSuggestion/", "category": "Chat" }, { "command": "github.copilot.chat.review.discard", "title": "%github.copilot.command.discardReviewSuggestion%", "icon": "$(close)", "category": "Chat" }, { "command": "github.copilot.chat.review.discardAndNext", "title": "%github.copilot.command.discardReviewSuggestionAndNext%", "icon": "$(close)", "category": "Chat" }, { "command": "github.copilot.chat.review.discardAll", "title": "%github.copilot.command.discardAllReviewSuggestion%", "icon": "$(close-all)", "category": "Chat" }, { "command": "github.copilot.chat.review.stagedChanges", "title": "%github.copilot.command.reviewStagedChanges%", "icon": "$(code-review)", "enablement": "github.copilot.chat.reviewDiff.enabled && !github.copilot.interactiveSession.disabled", "category": "Chat" }, { "command": "github.copilot.chat.review.unstagedChanges", "title": "%github.copilot.command.reviewUnstagedChanges%", "icon": "$(code-review)", "enablement": "github.copilot.chat.reviewDiff.enabled && !github.copilot.interactiveSession.disabled", "category": "Chat" }, { "command": "github.copilot.chat.review.changes", "title": "%github.copilot.command.reviewChanges%", "icon": "$(code-review)", "enablement": "github.copilot.chat.reviewDiff.enabled && !github.copilot.interactiveSession.disabled", "category": "Chat" }, { "command": "github.copilot.chat.review.stagedFileChange", "title": "%github.copilot.command.reviewFileChange%", "icon": "$(code-review)", "enablement": "github.copilot.chat.reviewDiff.enabled && !github.copilot.interactiveSession.disabled", "category": "Chat" }, { "command": "github.copilot.chat.review.unstagedFileChange", "title": "%github.copilot.command.reviewFileChange%", "icon": "$(code-review)", "enablement": "github.copilot.chat.reviewDiff.enabled && !github.copilot.interactiveSession.disabled", "category": "Chat" }, { "command": "github.copilot.chat.codeReview.run", "title": "%github.copilot.command.codeReviewRun%", "enablement": "github.copilot.chat.reviewDiff.enabled && !github.copilot.interactiveSession.disabled", "category": "Chat" }, { "command": "github.copilot.chat.review.previous", "title": "%github.copilot.command.gotoPreviousReviewSuggestion%", "icon": "$(arrow-up)", "category": "Chat" }, { "command": "github.copilot.chat.review.next", "title": "%github.copilot.command.gotoNextReviewSuggestion%", "icon": "$(arrow-down)", "category": "Chat" }, { "command": "github.copilot.chat.review.continueInInlineChat", "title": "%github.copilot.command.continueReviewInInlineChat%", "icon": "$(comment-discussion)", "category": "Chat" }, { "command": "github.copilot.chat.review.continueInChat", "title": "%github.copilot.command.continueReviewInChat%", "icon": "$(comment-discussion)", "category": "Chat" }, { "command": "github.copilot.chat.review.markHelpful", "title": "%github.copilot.command.helpfulReviewSuggestion%", "icon": "$(thumbsup)", "enablement": "!(commentThread =~ /markedAsHelpful/)", "category": "Chat" }, { "command": "github.copilot.chat.openUserPreferences", "title": "%github.copilot.command.openUserPreferences%", "category": "Chat", "enablement": "config.github.copilot.chat.enableUserPreferences" }, { "command": "github.copilot.chat.review.markUnhelpful", "title": "%github.copilot.command.unhelpfulReviewSuggestion%", "icon": "$(thumbsdown)", "enablement": "!(commentThread =~ /markedAsUnhelpful/)", "category": "Chat" }, { "command": "github.copilot.chat.generate", "title": "%github.copilot.command.generateThis%", "icon": "$(sparkle)", "enablement": "!github.copilot.interactiveSession.disabled && !editorReadonly", "category": "Chat" }, { "command": "github.copilot.chat.fix", "title": "%github.copilot.command.fixThis%", "enablement": "!github.copilot.interactiveSession.disabled && !editorReadonly", "category": "Chat" }, { "command": "github.copilot.interactiveSession.feedback", "title": "%github.copilot.command.sendChatFeedback%", "enablement": "github.copilot-chat.activated && !github.copilot.interactiveSession.disabled", "icon": "$(feedback)", "category": "Chat" }, { "command": "github.copilot.debug.workbenchState", "title": "%github.copilot.command.logWorkbenchState%", "category": "Developer" }, { "command": "github.copilot.debug.togglePowerSaveBlocker", "title": "%github.copilot.command.togglePowerSaveBlocker%", "category": "Developer" }, { "command": "github.copilot.debug.showChatLogView", "title": "%github.copilot.command.showChatLogView%", "category": "Developer" }, { "command": "github.copilot.debug.showOutputChannel", "title": "%github.copilot.command.showOutputChannel%", "category": "Developer" }, { "command": "github.copilot.debug.showContextInspectorView", "title": "%github.copilot.command.showContextInspectorView%", "icon": "$(inspect)", "category": "Developer" }, { "command": "github.copilot.debug.validateNesRename", "title": "%github.copilot.command.validateNesRename%", "category": "Developer" }, { "command": "github.copilot.debug.resetVirtualToolGroups", "title": "%github.copilot.command.resetVirtualToolGroups%", "icon": "$(inspect)", "category": "Developer" }, { "command": "github.copilot.debug.extensionState", "title": "%github.copilot.command.extensionState%", "category": "Developer" }, { "command": "github.copilot.chat.tools.memory.showMemories", "title": "%github.copilot.command.showMemories%", "category": "Chat" }, { "command": "github.copilot.chat.tools.memory.clearMemories", "title": "%github.copilot.command.clearMemories%", "category": "Chat" }, { "command": "github.copilot.terminal.explainTerminalLastCommand", "title": "%github.copilot.command.explainTerminalLastCommand%", "category": "Chat" }, { "command": "github.copilot.git.generateCommitMessage", "title": "%github.copilot.git.generateCommitMessage%", "icon": "$(sparkle)", "enablement": "!github.copilot.interactiveSession.disabled", "category": "Chat" }, { "command": "github.copilot.git.resolveMergeConflicts", "title": "%github.copilot.git.resolveMergeConflicts%", "icon": "$(chat-sparkle)", "enablement": "!github.copilot.interactiveSession.disabled", "category": "Chat" }, { "command": "github.copilot.devcontainer.generateDevContainerConfig", "title": "%github.copilot.devcontainer.generateDevContainerConfig%", "category": "Chat" }, { "command": "github.copilot.tests.fixTestFailure", "icon": "$(sparkle)", "title": "%github.copilot.command.fixTestFailure%", "category": "Chat" }, { "command": "github.copilot.tests.fixTestFailure.fromInline", "icon": "$(sparkle)", "title": "%github.copilot.command.fixTestFailure%" }, { "command": "github.copilot.chat.attachFile", "title": "%github.copilot.chat.attachFile%", "category": "Chat" }, { "command": "github.copilot.chat.attachSelection", "title": "%github.copilot.chat.attachSelection%", "icon": "$(comment-discussion)", "category": "Chat" }, { "command": "github.copilot.debug.collectDiagnostics", "title": "%github.copilot.command.collectDiagnostics%", "category": "Developer" }, { "command": "github.copilot.debug.showNodeSystemCertificatesErrors", "title": "%github.copilot.command.showNodeSystemCertificatesErrors%", "category": "Developer" }, { "command": "github.copilot.debug.inlineEdit.clearCache", "title": "%github.copilot.command.inlineEdit.clearCache%", "category": "Developer" }, { "command": "github.copilot.debug.inlineEdit.reportNotebookNESIssue", "title": "%github.copilot.command.inlineEdit.reportNotebookNESIssue%", "enablement": "config.github.copilot.chat.advanced.notebook.alternativeNESFormat.enabled || github.copilot.chat.enableEnhancedNotebookNES", "category": "Developer" }, { "command": "github.copilot.debug.generateSTest", "title": "%github.copilot.command.generateSTest%", "enablement": "github.copilot.debugReportFeedback", "category": "Developer" }, { "command": "github.copilot.open.walkthrough", "title": "%github.copilot.command.openWalkthrough%", "category": "Chat" }, { "command": "github.copilot.debug.generateInlineEditTests", "title": "Generate Inline Edit Tests", "category": "Chat", "enablement": "resourceScheme == 'ccreq'" }, { "command": "github.copilot.buildLocalWorkspaceIndex", "title": "%github.copilot.command.buildLocalWorkspaceIndex%", "category": "Chat", "enablement": "github.copilot-chat.activated" }, { "command": "github.copilot.buildRemoteWorkspaceIndex", "title": "%github.copilot.command.buildRemoteWorkspaceIndex%", "category": "Chat", "enablement": "github.copilot-chat.activated" }, { "command": "github.copilot.deleteExternalIngestWorkspaceIndex", "title": "%github.copilot.command.deleteExternalIngestWorkspaceIndex%", "category": "Developer", "enablement": "github.copilot-chat.activated" }, { "command": "github.copilot.report", "title": "Report Issue", "category": "Chat" }, { "command": "github.copilot.chat.rerunWithCopilotDebug", "title": "%github.copilot.command.rerunWithCopilotDebug%", "category": "Chat" }, { "command": "github.copilot.chat.startCopilotDebugCommand", "title": "Start Copilot Debug" }, { "command": "github.copilot.chat.clearTemporalContext", "title": "Clear Temporal Context", "category": "Developer" }, { "command": "github.copilot.search.markHelpful", "title": "Helpful", "icon": "$(thumbsup)", "enablement": "!github.copilot.search.feedback.sent" }, { "command": "github.copilot.search.markUnhelpful", "title": "Unhelpful", "icon": "$(thumbsdown)", "enablement": "!github.copilot.search.feedback.sent" }, { "command": "github.copilot.search.feedback", "title": "Feedback", "icon": "$(feedback)", "enablement": "!github.copilot.search.feedback.sent" }, { "command": "github.copilot.chat.debug.showElements", "title": "Show Rendered Elements" }, { "command": "github.copilot.chat.debug.hideElements", "title": "Hide Rendered Elements" }, { "command": "github.copilot.chat.debug.showTools", "title": "Show Tools" }, { "command": "github.copilot.chat.debug.hideTools", "title": "Hide Tools" }, { "command": "github.copilot.chat.debug.showNesRequests", "title": "Show NES Requests" }, { "command": "github.copilot.chat.debug.hideNesRequests", "title": "Hide NES Requests" }, { "command": "github.copilot.chat.debug.showGhostRequests", "title": "Show Ghost Requests" }, { "command": "github.copilot.chat.debug.hideGhostRequests", "title": "Hide Ghost Requests" }, { "command": "github.copilot.chat.debug.showRawRequestBody", "title": "Show Raw Request Body" }, { "command": "github.copilot.chat.debug.exportLogItem", "title": "Export as...", "icon": "$(export)" }, { "command": "github.copilot.chat.debug.exportPromptArchive", "title": "Export All as Archive...", "icon": "$(archive)" }, { "command": "github.copilot.chat.debug.exportPromptLogsAsJson", "title": "Export All as JSON...", "icon": "$(export)" }, { "command": "github.copilot.chat.debug.exportAllPromptLogsAsJson", "title": "Export All Prompt Logs as JSON...", "icon": "$(export)" }, { "command": "github.copilot.chat.debug.exportTrajectories", "title": "Export Agent Trajectories", "category": "Chat" }, { "command": "github.copilot.chat.debug.exportSingleTrajectory", "title": "Export Trajectory...", "category": "Chat" }, { "command": "github.copilot.nes.captureExpected.start", "title": "Record Expected Edit (NES)", "category": "Copilot" }, { "command": "github.copilot.nes.captureExpected.confirm", "title": "Confirm and Save Expected Edit Capture", "category": "Copilot" }, { "command": "github.copilot.nes.captureExpected.abort", "title": "Cancel Expected Edit Capture", "category": "Copilot" }, { "command": "github.copilot.nes.captureExpected.submit", "title": "Submit NES Captures", "category": "Copilot" }, { "command": "github.copilot.chat.showAsChatSession", "title": "Show as chat session", "icon": "$(chat-sparkle)" }, { "command": "github.copilot.debug.collectWorkspaceIndexDiagnostics", "title": "%github.copilot.command.collectWorkspaceIndexDiagnostics%", "category": "Developer" }, { "command": "github.copilot.chat.mcp.setup.check", "title": "MCP Check: is supported" }, { "command": "github.copilot.chat.mcp.setup.validatePackage", "title": "MCP Check: validate package" }, { "command": "github.copilot.chat.mcp.setup.flow", "title": "MCP Check: do prompts" }, { "command": "github.copilot.chat.generateAltText", "title": "Generate/Refine Alt Text" }, { "command": "github.copilot.chat.notebook.enableFollowCellExecution", "title": "Enable Follow Cell Execution from Chat", "shortTitle": "Follow", "icon": "$(pinned)" }, { "command": "github.copilot.chat.notebook.disableFollowCellExecution", "title": "Disable Follow Cell Execution from Chat", "shortTitle": "Unfollow", "icon": "$(pinned-dirty)" }, { "command": "github.copilot.cloud.resetWorkspaceConfirmations", "title": "%github.copilot.command.resetCloudAgentWorkspaceConfirmations%" }, { "command": "github.copilot.cloud.sessions.openInBrowser", "title": "%github.copilot.command.openCopilotAgentSessionsInBrowser%", "icon": "$(link-external)" }, { "command": "github.copilot.cloud.sessions.proxy.closeChatSessionPullRequest", "title": "%github.copilot.command.closeChatSessionPullRequest.title%" }, { "command": "github.copilot.cloud.sessions.installPRExtension", "title": "%github.copilot.command.installPRExtension.title%", "icon": "$(extensions)" }, { "command": "github.copilot.chat.openSuggestionsPanel", "title": "Open Completions Panel", "enablement": "github.copilot.extensionUnification.activated && !isWeb", "category": "GitHub Copilot" }, { "command": "github.copilot.chat.toggleStatusMenu", "title": "Open Status Menu", "enablement": "github.copilot.extensionUnification.activated", "category": "GitHub Copilot" }, { "command": "github.copilot.chat.completions.disable", "title": "Disable Inline Suggestions", "enablement": "github.copilot.extensionUnification.activated && github.copilot.activated && config.editor.inlineSuggest.enabled && github.copilot.completions.enabled", "category": "GitHub Copilot" }, { "command": "github.copilot.chat.completions.enable", "title": "Enable Inline Suggestions", "enablement": "github.copilot.extensionUnification.activated && github.copilot.activated && !(config.editor.inlineSuggest.enabled && github.copilot.completions.enabled)", "category": "GitHub Copilot" }, { "command": "github.copilot.chat.completions.toggle", "title": "Toggle (Enable/Disable) Inline Suggestions", "enablement": "github.copilot.extensionUnification.activated && github.copilot.activated", "category": "GitHub Copilot" }, { "command": "github.copilot.chat.openModelPicker", "title": "Change Completions Model", "category": "GitHub Copilot", "enablement": "github.copilot.extensionUnification.activated && !isWeb && github.copilot.completions.hasMultipleModels" }, { "command": "github.copilot.chat.applyCopilotCLIAgentSessionChanges", "title": "%github.copilot.command.applyCopilotCLIAgentSessionChanges%", "enablement": "!chatSessionRequestInProgress", "category": "GitHub Copilot" }, { "command": "github.copilot.chat.applyCopilotCLIAgentSessionChanges.apply", "title": "%github.copilot.chat.applyCopilotCLIAgentSessionChanges.apply%", "enablement": "!chatSessionRequestInProgress", "icon": "$(git-stash-pop)", "category": "GitHub Copilot" }, { "command": "github.copilot.chat.mergeCopilotCLIAgentSessionChanges.merge", "title": "%github.copilot.chat.mergeCopilotCLIAgentSessionChanges.merge%", "enablement": "!chatSessionRequestInProgress", "icon": "$(git-merge)", "category": "GitHub Copilot" }, { "command": "github.copilot.chat.mergeCopilotCLIAgentSessionChanges.mergeAndSync", "title": "%github.copilot.chat.mergeCopilotCLIAgentSessionChanges.mergeAndSync%", "enablement": "!chatSessionRequestInProgress", "icon": "$(sync)", "category": "GitHub Copilot" }, { "command": "github.copilot.chat.updateCopilotCLIAgentSessionChanges.update", "title": "%github.copilot.chat.updateCopilotCLIAgentSessionChanges.update%", "enablement": "!chatSessionRequestInProgress", "icon": "$(download)", "category": "GitHub Copilot" }, { "command": "github.copilot.chat.createPullRequestCopilotCLIAgentSession.createPR", "title": "%github.copilot.chat.createPullRequestCopilotCLIAgentSession.createPR%", "enablement": "!chatSessionRequestInProgress", "icon": "$(git-pull-request-create)", "category": "GitHub Copilot" }, { "command": "github.copilot.chat.createPullRequestCopilotCLIAgentSession.updatePR", "title": "%github.copilot.chat.createPullRequestCopilotCLIAgentSession.updatePR%", "enablement": "!chatSessionRequestInProgress", "icon": "$(git-pull-request)", "category": "GitHub Copilot" }, { "command": "github.copilot.chat.createDraftPullRequestCopilotCLIAgentSession.createDraftPR", "title": "%github.copilot.chat.createDraftPullRequestCopilotCLIAgentSession.createDraftPR%", "enablement": "!chatSessionRequestInProgress", "icon": "$(git-pull-request-draft)", "category": "GitHub Copilot" }, { "command": "github.copilot.chat.openPullRequestCopilotCLIAgentSession.openPR", "title": "%github.copilot.chat.openPullRequestCopilotCLIAgentSession.openPR%", "enablement": "!chatSessionRequestInProgress", "icon": "$(git-pull-request)", "category": "GitHub Copilot" }, { "command": "github.copilot.chat.copilotCLI.addFileReference", "title": "%github.copilot.command.chat.copilotCLI.addFileReference%", "enablement": "github.copilot.chat.copilotCLI.hasSession", "category": "Copilot CLI" }, { "command": "github.copilot.chat.copilotCLI.addSelection", "title": "%github.copilot.command.chat.copilotCLI.addSelection%", "enablement": "github.copilot.chat.copilotCLI.hasSession", "category": "Copilot CLI" }, { "command": "github.copilot.chat.copilotCLI.acceptDiff", "title": "%github.copilot.command.chat.copilotCLI.acceptDiff%", "enablement": "github.copilot.chat.copilotCLI.hasActiveDiff", "icon": "$(check)", "category": "Copilot CLI" }, { "command": "github.copilot.chat.copilotCLI.rejectDiff", "title": "%github.copilot.command.chat.copilotCLI.rejectDiff%", "enablement": "github.copilot.chat.copilotCLI.hasActiveDiff", "icon": "$(close)", "category": "Copilot CLI" }, { "command": "github.copilot.chat.checkoutPullRequestReroute", "title": "%github.copilot.command.checkoutPullRequestReroute.title%", "icon": "$(git-pull-request)", "category": "GitHub Pull Request" }, { "command": "github.copilot.chat.cloudSessions.openRepository", "title": "%github.copilot.command.cloudSessions.openRepository.title%", "icon": "$(repo)", "category": "GitHub Copilot" }, { "command": "github.copilot.chat.cloudSessions.clearCaches", "title": "%github.copilot.command.cloudSessions.clearCaches.title%", "category": "GitHub Copilot" } ], "configuration": [ { "title": "GitHub Copilot Chat", "id": "stable", "properties": { "github.copilot.chat.backgroundAgent.enabled": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.backgroundAgent.enabled%" }, "github.copilot.chat.cloudAgent.enabled": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.cloudAgent.enabled%" }, "github.copilot.chat.codeGeneration.useInstructionFiles": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.codeGeneration.useInstructionFiles%" }, "github.copilot.editor.enableCodeActions": { "type": "boolean", "default": true, "description": "%github.copilot.config.enableCodeActions%" }, "github.copilot.renameSuggestions.triggerAutomatically": { "type": "boolean", "default": true, "description": "%github.copilot.config.renameSuggestions.triggerAutomatically%" }, "github.copilot.chat.localeOverride": { "type": "string", "enum": [ "auto", "en", "fr", "it", "de", "es", "ru", "zh-CN", "zh-TW", "ja", "ko", "cs", "pt-br", "tr", "pl" ], "enumDescriptions": [ "Use VS Code's configured display language", "English", "français", "italiano", "Deutsch", "español", "русский", "中文(简体)", "中文(繁體)", "日本語", "한국어", "čeština", "português", "Türkçe", "polski" ], "default": "auto", "markdownDescription": "%github.copilot.config.localeOverride%" }, "github.copilot.chat.terminalChatLocation": { "type": "string", "default": "chatView", "markdownDescription": "%github.copilot.config.terminalChatLocation%", "markdownEnumDescriptions": [ "%github.copilot.config.terminalChatLocation.chatView%", "%github.copilot.config.terminalChatLocation.quickChat%", "%github.copilot.config.terminalChatLocation.terminal%" ], "enum": [ "chatView", "quickChat", "terminal" ] }, "github.copilot.chat.scopeSelection": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.scopeSelection%" }, "github.copilot.chat.useProjectTemplates": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.useProjectTemplates%" }, "github.copilot.nextEditSuggestions.enabled": { "type": "boolean", "default": true, "tags": [ "nextEditSuggestions", "onExp" ], "markdownDescription": "%github.copilot.nextEditSuggestions.enabled%", "scope": "language-overridable" }, "github.copilot.nextEditSuggestions.extendedRange": { "type": "boolean", "default": false, "tags": [ "nextEditSuggestions", "onExp" ], "markdownDescription": "%github.copilot.nextEditSuggestions.extendedRange%" }, "github.copilot.nextEditSuggestions.fixes": { "type": "boolean", "default": true, "tags": [ "nextEditSuggestions", "onExp" ], "markdownDescription": "%github.copilot.nextEditSuggestions.fixes%", "scope": "language-overridable" }, "github.copilot.nextEditSuggestions.allowWhitespaceOnlyChanges": { "type": "boolean", "default": true, "tags": [ "nextEditSuggestions", "onExp" ], "markdownDescription": "%github.copilot.nextEditSuggestions.allowWhitespaceOnlyChanges%", "scope": "language-overridable" }, "github.copilot.chat.agent.autoFix": { "type": "boolean", "default": false, "description": "%github.copilot.config.autoFix%", "tags": [ "onExp" ] }, "github.copilot.chat.rateLimitAutoSwitchToAuto": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.rateLimitAutoSwitchToAuto%", "tags": [ "onExp" ] }, "github.copilot.chat.customInstructionsInSystemMessage": { "type": "boolean", "default": true, "description": "%github.copilot.config.customInstructionsInSystemMessage%" }, "github.copilot.chat.organizationCustomAgents.enabled": { "type": "boolean", "default": true, "description": "%github.copilot.config.organizationCustomAgents.enabled%" }, "github.copilot.chat.organizationInstructions.enabled": { "type": "boolean", "default": true, "description": "%github.copilot.config.organizationInstructions.enabled%" }, "github.copilot.chat.additionalReadAccessPaths": { "type": "array", "default": [], "items": { "type": "string" }, "markdownDescription": "%github.copilot.config.additionalReadAccessPaths%", "scope": "window" }, "github.copilot.chat.agent.currentEditorContext.enabled": { "type": "boolean", "default": true, "description": "%github.copilot.config.agent.currentEditorContext.enabled%" }, "github.copilot.enable": { "type": "object", "scope": "window", "default": { "*": true, "plaintext": false, "markdown": false, "scminput": false }, "additionalProperties": { "type": "boolean" }, "markdownDescription": "Enable or disable auto triggering of Copilot completions for specified [languages](https://code.visualstudio.com/docs/languages/identifiers). You can still trigger suggestions manually using `Alt + \\`" }, "github.copilot.selectedCompletionModel": { "type": "string", "default": "", "markdownDescription": "The currently selected completion model ID. To select from a list of available models, use the __\"Change Completions Model\"__ command or open the model picker (from the Copilot menu in the VS Code title bar, select __\"Configure Code Completions\"__ then __\"Change Completions Model\"__. The value must be a valid model ID. An empty value indicates that the default model will be used." }, "github.copilot.chat.claudeAgent.enabled": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.claudeAgent.enabled%" }, "github.copilot.chat.claudeAgent.allowDangerouslySkipPermissions": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.claudeAgent.allowDangerouslySkipPermissions%" }, "github.copilot.chat.reviewAgent.enabled": { "type": "boolean", "default": true, "description": "%github.copilot.config.reviewAgent.enabled%" }, "github.copilot.chat.reviewSelection.enabled": { "type": "boolean", "default": true, "description": "%github.copilot.config.reviewSelection.enabled%" }, "github.copilot.chat.reviewSelection.instructions": { "type": "array", "items": { "oneOf": [ { "type": "object", "markdownDescription": "%github.copilot.config.reviewSelection.instruction.file%", "properties": { "file": { "type": "string", "examples": [ ".copilot-review-instructions.md" ] }, "language": { "type": "string" } }, "examples": [ { "file": ".copilot-review-instructions.md" } ], "required": [ "file" ] }, { "type": "object", "markdownDescription": "%github.copilot.config.reviewSelection.instruction.text%", "properties": { "text": { "type": "string", "examples": [ "Use underscore for field names." ] }, "language": { "type": "string" } }, "required": [ "text" ], "examples": [ { "text": "Use underscore for field names." }, { "text": "Resolve all TODO tasks." } ] } ] }, "default": [], "markdownDescription": "%github.copilot.config.reviewSelection.instructions%", "examples": [ [ { "file": ".copilot-review-instructions.md" }, { "text": "Resolve all TODO tasks." } ] ] }, "github.copilot.chat.anthropic.useMessagesApi": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.useMessagesApi%", "tags": [ "onExp" ] } } }, { "id": "preview", "properties": { "github.copilot.chat.copilotDebugCommand.enabled": { "type": "boolean", "default": true, "tags": [ "preview" ], "description": "%github.copilot.chat.copilotDebugCommand.enabled%" }, "github.copilot.chat.codesearch.enabled": { "type": "boolean", "default": false, "tags": [ "preview" ], "markdownDescription": "%github.copilot.config.codesearch.enabled%" }, "github.copilot.chat.copilotMemory.enabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.copilotMemory.enabled%", "tags": [ "preview" ] }, "github.copilot.chat.tools.memory.enabled": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.tools.memory.enabled%", "tags": [ "preview" ] }, "github.copilot.chat.tools.viewImage.enabled": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.tools.viewImage.enabled%", "tags": [ "preview", "onExp" ] }, "github.copilot.chat.anthropic.thinking.budgetTokens": { "type": "number", "markdownDescription": "%github.copilot.config.anthropic.thinking.budgetTokens%", "minimum": 0, "maximum": 32000, "default": 16000, "tags": [ "preview", "onExp" ] }, "github.copilot.chat.anthropic.thinking.forceExtendedThinking": { "type": "boolean", "markdownDescription": "%github.copilot.config.anthropic.thinking.forceExtendedThinking%", "default": false, "tags": [ "preview", "onExp" ] }, "github.copilot.chat.backgroundCompaction": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.backgroundCompaction%", "tags": [ "preview", "onExp" ] }, "github.copilot.chat.anthropic.toolSearchTool.enabled": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.anthropic.toolSearchTool.enabled%", "tags": [ "preview" ] }, "github.copilot.chat.anthropic.toolSearchTool.mode": { "type": "string", "enum": [ "server", "client" ], "default": "server", "markdownDescription": "%github.copilot.config.anthropic.toolSearchTool.mode%", "tags": [ "preview", "onExp" ] }, "github.copilot.chat.conversationTranscriptLookup.enabled": { "type": "boolean", "default": false, "description": "%github.copilot.config.conversationTranscriptLookup.enabled%", "tags": [ "preview", "onExp" ] } } }, { "id": "experimental", "properties": { "github.copilot.chat.getSearchViewResultsSkill.enabled": { "type": "boolean", "default": false, "description": "%github.copilot.config.getSearchViewResultsSkill.enabled%", "tags": [ "experimental", "onExp" ] }, "github.copilot.chat.githubMcpServer.enabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.githubMcpServer.enabled%", "tags": [ "experimental" ] }, "github.copilot.chat.githubMcpServer.toolsets": { "type": "array", "default": [ "default" ], "markdownDescription": "%github.copilot.config.githubMcpServer.toolsets%", "items": { "type": "string" }, "tags": [ "experimental" ] }, "github.copilot.chat.githubMcpServer.readonly": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.githubMcpServer.readonly%", "tags": [ "experimental" ] }, "github.copilot.chat.githubMcpServer.lockdown": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.githubMcpServer.lockdown%", "tags": [ "experimental" ] }, "github.copilot.chat.switchAgent.enabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.switchAgent.enabled%", "tags": [ "experimental", "onExp" ] }, "github.copilot.chat.imageUpload.enabled": { "type": "boolean", "default": true, "tags": [ "experimental", "onExp" ], "markdownDescription": "%github.copilot.config.imageUpload.enabled%" }, "github.copilot.chat.codeGeneration.instructions": { "markdownDeprecationMessage": "%github.copilot.config.codeGeneration.instructions.deprecated%", "type": "array", "items": { "oneOf": [ { "type": "object", "markdownDescription": "%github.copilot.config.codeGeneration.instruction.file%", "properties": { "file": { "type": "string", "examples": [ ".copilot-codeGeneration-instructions.md" ] }, "language": { "type": "string" } }, "examples": [ { "file": ".copilot-codeGeneration-instructions.md" } ], "required": [ "file" ] }, { "type": "object", "markdownDescription": "%github.copilot.config.codeGeneration.instruction.text%", "properties": { "text": { "type": "string", "examples": [ "Use underscore for field names." ] }, "language": { "type": "string" } }, "required": [ "text" ], "examples": [ { "text": "Use underscore for field names." }, { "text": "Always add a comment: 'Generated by Copilot'." } ] } ] }, "default": [], "markdownDescription": "%github.copilot.config.codeGeneration.instructions%", "examples": [ [ { "file": ".copilot-codeGeneration-instructions.md" }, { "text": "Always add a comment: 'Generated by Copilot'." } ] ], "tags": [ "experimental" ] }, "github.copilot.chat.testGeneration.instructions": { "markdownDeprecationMessage": "%github.copilot.config.testGeneration.instructions.deprecated%", "type": "array", "items": { "oneOf": [ { "type": "object", "markdownDescription": "%github.copilot.config.experimental.testGeneration.instruction.file%", "properties": { "file": { "type": "string", "examples": [ ".copilot-test-instructions.md" ] }, "language": { "type": "string" } }, "examples": [ { "file": ".copilot-test-instructions.md" } ], "required": [ "file" ] }, { "type": "object", "markdownDescription": "%github.copilot.config.experimental.testGeneration.instruction.text%", "properties": { "text": { "type": "string", "examples": [ "Use suite and test instead of describe and it." ] }, "language": { "type": "string" } }, "required": [ "text" ], "examples": [ { "text": "Always try uniting related tests in a suite." } ] } ] }, "default": [], "markdownDescription": "%github.copilot.config.testGeneration.instructions%", "examples": [ [ { "file": ".copilot-test-instructions.md" }, { "text": "Always try uniting related tests in a suite." } ] ], "tags": [ "experimental" ] }, "github.copilot.chat.commitMessageGeneration.instructions": { "type": "array", "items": { "oneOf": [ { "type": "object", "markdownDescription": "%github.copilot.config.commitMessageGeneration.instruction.file%", "properties": { "file": { "type": "string", "examples": [ ".copilot-commit-message-instructions.md" ] } }, "examples": [ { "file": ".copilot-commit-message-instructions.md" } ], "required": [ "file" ] }, { "type": "object", "markdownDescription": "%github.copilot.config.commitMessageGeneration.instruction.text%", "properties": { "text": { "type": "string", "examples": [ "Use conventional commit message format." ] } }, "required": [ "text" ], "examples": [ { "text": "Use conventional commit message format." } ] } ] }, "default": [], "markdownDescription": "%github.copilot.config.commitMessageGeneration.instructions%", "examples": [ [ { "file": ".copilot-commit-message-instructions.md" }, { "text": "Use conventional commit message format." } ] ], "tags": [ "experimental" ] }, "github.copilot.chat.pullRequestDescriptionGeneration.instructions": { "type": "array", "items": { "oneOf": [ { "type": "object", "markdownDescription": "%github.copilot.config.pullRequestDescriptionGeneration.instruction.file%", "properties": { "file": { "type": "string", "examples": [ ".copilot-pull-request-description-instructions.md" ] } }, "examples": [ { "file": ".copilot-pull-request-description-instructions.md" } ], "required": [ "file" ] }, { "type": "object", "markdownDescription": "%github.copilot.config.pullRequestDescriptionGeneration.instruction.text%", "properties": { "text": { "type": "string", "examples": [ "Include every commit message in the pull request description." ] } }, "required": [ "text" ], "examples": [ { "text": "Include every commit message in the pull request description." } ] } ] }, "default": [], "markdownDescription": "%github.copilot.config.pullRequestDescriptionGeneration.instructions%", "examples": [ [ { "file": ".copilot-pull-request-description-instructions.md" }, { "text": "Use conventional commit message format." } ] ], "tags": [ "experimental" ] }, "github.copilot.chat.setupTests.enabled": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.setupTests.enabled%", "tags": [ "experimental" ] }, "github.copilot.chat.languageContext.typescript.enabled": { "type": "boolean", "default": true, "scope": "resource", "tags": [ "experimental", "onExP" ], "markdownDescription": "%github.copilot.chat.languageContext.typescript.enabled%" }, "github.copilot.chat.languageContext.typescript.items": { "type": "string", "enum": [ "minimal", "double", "fillHalf", "fill" ], "default": "double", "scope": "resource", "tags": [ "experimental", "onExP" ], "markdownDescription": "%github.copilot.chat.languageContext.typescript.items%" }, "github.copilot.chat.languageContext.typescript.includeDocumentation": { "type": "boolean", "default": false, "scope": "resource", "tags": [ "experimental", "onExP" ], "markdownDescription": "%github.copilot.chat.languageContext.typescript.includeDocumentation%" }, "github.copilot.chat.languageContext.typescript.cacheTimeout": { "type": "number", "default": 500, "scope": "resource", "tags": [ "experimental", "onExP" ], "markdownDescription": "%github.copilot.chat.languageContext.typescript.cacheTimeout%" }, "github.copilot.chat.languageContext.fix.typescript.enabled": { "type": "boolean", "default": false, "scope": "resource", "tags": [ "experimental", "onExP" ], "markdownDescription": "%github.copilot.chat.languageContext.fix.typescript.enabled%" }, "github.copilot.chat.languageContext.inline.typescript.enabled": { "type": "boolean", "default": false, "scope": "resource", "tags": [ "experimental", "onExP" ], "markdownDescription": "%github.copilot.chat.languageContext.inline.typescript.enabled%" }, "github.copilot.chat.newWorkspaceCreation.enabled": { "type": "boolean", "default": true, "tags": [ "experimental" ], "description": "%github.copilot.config.newWorkspaceCreation.enabled%" }, "github.copilot.chat.newWorkspace.useContext7": { "type": "boolean", "default": false, "tags": [ "experimental" ], "markdownDescription": "%github.copilot.config.newWorkspace.useContext7%" }, "github.copilot.chat.notebook.followCellExecution.enabled": { "type": "boolean", "default": false, "tags": [ "experimental" ], "description": "%github.copilot.config.notebook.followCellExecution%" }, "github.copilot.chat.notebook.enhancedNextEditSuggestions.enabled": { "type": "boolean", "default": false, "tags": [ "experimental", "onExp" ], "description": "%github.copilot.config.notebook.enhancedNextEditSuggestions%" }, "github.copilot.chat.summarizeAgentConversationHistory.enabled": { "type": "boolean", "default": true, "tags": [ "experimental" ], "description": "%github.copilot.config.summarizeAgentConversationHistory.enabled%" }, "github.copilot.chat.virtualTools.threshold": { "type": "number", "minimum": 0, "maximum": 128, "default": 128, "tags": [ "experimental" ], "markdownDescription": "%github.copilot.config.virtualTools.threshold%" }, "github.copilot.chat.alternateGptPrompt.enabled": { "type": "boolean", "default": false, "tags": [ "experimental" ], "description": "%github.copilot.config.alternateGptPrompt.enabled%" }, "github.copilot.chat.alternateGeminiModelFPrompt.enabled": { "type": "boolean", "default": false, "tags": [ "experimental", "onExp" ], "description": "%github.copilot.config.alternateGeminiModelFPrompt.enabled%" }, "github.copilot.chat.anthropic.contextEditing.mode": { "type": "string", "default": "off", "markdownDescription": "%github.copilot.config.anthropic.contextEditing.mode%", "tags": [ "experimental", "onExp" ], "enum": [ "off", "clear-thinking", "clear-tooluse", "clear-both" ] }, "github.copilot.chat.anthropic.promptOptimization": { "type": "string", "default": "control", "markdownDescription": "%github.copilot.config.anthropic.promptOptimization%", "tags": [ "experimental", "onExp" ], "enum": [ "control", "combined", "split" ] }, "github.copilot.chat.responsesApiReasoningSummary": { "type": "string", "default": "detailed", "markdownDescription": "%github.copilot.config.responsesApiReasoningSummary%", "tags": [ "experimental", "onExp" ], "enum": [ "off", "detailed" ] }, "github.copilot.chat.responsesApiContextManagement.enabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.responsesApiContextManagement.enabled%", "tags": [ "experimental", "onExp" ] }, "github.copilot.chat.updated53CodexPrompt.enabled": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.updated53CodexPrompt.enabled%", "tags": [ "experimental", "onExp" ] }, "github.copilot.chat.anthropic.tools.websearch.enabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.anthropic.tools.websearch.enabled%", "tags": [ "experimental", "onExp" ] }, "github.copilot.chat.anthropic.tools.websearch.maxUses": { "type": "number", "default": 5, "markdownDescription": "%github.copilot.config.anthropic.tools.websearch.maxUses%", "minimum": 1, "maximum": 20, "tags": [ "experimental" ] }, "github.copilot.chat.anthropic.tools.websearch.allowedDomains": { "type": "array", "default": [], "markdownDescription": "%github.copilot.config.anthropic.tools.websearch.allowedDomains%", "items": { "type": "string" }, "tags": [ "experimental" ] }, "github.copilot.chat.anthropic.tools.websearch.blockedDomains": { "type": "array", "default": [], "markdownDescription": "%github.copilot.config.anthropic.tools.websearch.blockedDomains%", "items": { "type": "string" }, "tags": [ "experimental" ] }, "github.copilot.chat.anthropic.tools.websearch.userLocation": { "type": [ "object", "null" ], "default": null, "markdownDescription": "%github.copilot.config.anthropic.tools.websearch.userLocation%", "properties": { "city": { "type": "string", "description": "City name (e.g., 'San Francisco')" }, "region": { "type": "string", "description": "State or region (e.g., 'California')" }, "country": { "type": "string", "description": "ISO country code (e.g., 'US')" }, "timezone": { "type": "string", "description": "IANA timezone identifier (e.g., 'America/Los_Angeles')" } }, "tags": [ "experimental" ] }, "github.copilot.chat.completionsFetcher": { "type": [ "string", "null" ], "markdownDescription": "%github.copilot.config.completionsFetcher%", "tags": [ "experimental", "onExp" ], "enum": [ "electron-fetch", "node-fetch" ] }, "github.copilot.chat.nesFetcher": { "type": [ "string", "null" ], "markdownDescription": "%github.copilot.config.nesFetcher%", "tags": [ "experimental", "onExp" ], "enum": [ "electron-fetch", "node-fetch" ] }, "github.copilot.chat.planAgent.additionalTools": { "type": "array", "items": { "type": "string" }, "default": [], "scope": "resource", "markdownDescription": "%github.copilot.config.planAgent.additionalTools%", "tags": [ "experimental" ] }, "github.copilot.chat.implementAgent.model": { "type": "string", "default": "", "scope": "resource", "markdownDescription": "%github.copilot.config.implementAgent.model%", "tags": [ "experimental" ] }, "github.copilot.chat.askAgent.additionalTools": { "type": "array", "items": { "type": "string" }, "default": [], "scope": "resource", "markdownDescription": "%github.copilot.config.askAgent.additionalTools%", "tags": [ "experimental" ] }, "github.copilot.chat.askAgent.model": { "type": "string", "default": "", "scope": "resource", "markdownDescription": "%github.copilot.config.askAgent.model%", "tags": [ "experimental" ] }, "github.copilot.chat.exploreAgent.model": { "type": "string", "default": "", "scope": "resource", "markdownDescription": "%github.copilot.config.exploreAgent.model%", "tags": [ "experimental" ] } } }, { "id": "advanced", "properties": { "github.copilot.chat.anthropic.promptCaching.extendedTtl": { "type": "boolean", "markdownDescription": "%github.copilot.config.anthropic.promptCaching.extendedTtl%", "default": false, "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.installExtensionSkill.enabled": { "type": "boolean", "default": false, "tags": [ "advanced", "experimental", "onExp" ], "description": "%github.copilot.config.installExtensionSkill.enabled%" }, "github.copilot.chat.projectSetupInfoSkill.enabled": { "type": "boolean", "default": false, "tags": [ "advanced", "experimental", "onExp" ], "description": "%github.copilot.config.projectSetupInfoSkill.enabled%" }, "github.copilot.chat.debug.promptOverrideFile": { "type": [ "string", "null" ], "default": null, "markdownDescription": "Path to a YAML file that overrides the system prompt and/or tool descriptions sent to the model.\n\n**Note**: This is an advanced debugging setting.", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.edits.gemini3MultiReplaceString": { "type": "boolean", "default": false, "markdownDescription": "Enable the modern `multi_replace_string_in_file` edit tool when generating edits with Gemini 3 models.", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.projectLabels.expanded": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.projectLabels.expanded%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.projectLabels.chat": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.projectLabels.chat%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.projectLabels.inline": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.projectLabels.inline%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.workspace.maxLocalIndexSize": { "type": "number", "default": 100000, "markdownDescription": "%github.copilot.config.workspace.maxLocalIndexSize%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.workspace.enableFullWorkspace": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.workspace.enableFullWorkspace%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.workspace.enableCodeSearch": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.workspace.enableCodeSearch%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.workspace.enableEmbeddingsSearch": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.workspace.enableEmbeddingsSearch%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.workspace.preferredEmbeddingsModel": { "type": "string", "default": "", "markdownDescription": "%github.copilot.config.workspace.preferredEmbeddingsModel%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.workspace.prototypeAdoCodeSearchEndpointOverride": { "type": "string", "default": "", "markdownDescription": "%github.copilot.config.workspace.prototypeAdoCodeSearchEndpointOverride%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.feedback.onChange": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.feedback.onChange%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.review.intent": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.review.intent%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.notebook.summaryExperimentEnabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.notebook.summaryExperimentEnabled%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.notebook.variableFilteringEnabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.notebook.variableFilteringEnabled%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.notebook.alternativeFormat": { "type": "string", "default": "xml", "enum": [ "xml", "markdown" ], "markdownDescription": "%github.copilot.config.notebook.alternativeFormat%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.notebook.alternativeNESFormat.enabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.notebook.alternativeNESFormat.enabled%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.debugTerminalCommandPatterns": { "type": "array", "default": [], "items": { "type": "string" }, "markdownDescription": "%github.copilot.config.debugTerminalCommandPatterns%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.localWorkspaceRecording.enabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.localWorkspaceRecording.enabled%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.editRecording.enabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.editRecording.enabled%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.inlineChat.selectionRatioThreshold": { "type": "number", "default": 0, "markdownDescription": "%github.copilot.config.inlineChat.selectionRatioThreshold%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.debug.requestLogger.maxEntries": { "type": "number", "default": 100, "markdownDescription": "%github.copilot.config.debug.requestLogger.maxEntries%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.inlineEdits.diagnosticsContextProvider.enabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.inlineEdits.diagnosticsContextProvider.enabled%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.inlineEdits.chatSessionContextProvider.enabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.inlineEdits.chatSessionContextProvider.enabled%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.codesearch.agent.enabled": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.codesearch.agent.enabled%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.agent.temperature": { "type": [ "number", "null" ], "markdownDescription": "%github.copilot.config.agent.temperature%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.agent.omitFileAttachmentContents": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.agent.omitFileAttachmentContents%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.agent.largeToolResultsToDisk.enabled": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.agent.largeToolResultsToDisk.enabled%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.agent.largeToolResultsToDisk.thresholdBytes": { "type": "number", "default": 8192, "markdownDescription": "%github.copilot.config.agent.largeToolResultsToDisk.thresholdBytes%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.instantApply.shortContextModelName": { "type": "string", "default": "gpt-4o-instant-apply-full-ft-v66-short", "markdownDescription": "%github.copilot.config.instantApply.shortContextModelName%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.instantApply.shortContextLimit": { "type": "number", "default": 8000, "markdownDescription": "%github.copilot.config.instantApply.shortContextLimit%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.enableUserPreferences": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.enableUserPreferences%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.executionSubagent.enabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.executionSubagent.enabled%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.executionSubagent.model": { "type": "string", "default": "", "markdownDescription": "%github.copilot.config.executionSubagent.model%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.executionSubagent.toolCallLimit": { "type": "number", "default": 5, "markdownDescription": "%github.copilot.config.executionSubagent.toolCallLimit%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.summarizeAgentConversationHistoryThreshold": { "type": [ "number", "null" ], "markdownDescription": "%github.copilot.config.summarizeAgentConversationHistoryThreshold%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.agentHistorySummarizationMode": { "type": [ "string", "null" ], "markdownDescription": "%github.copilot.config.agentHistorySummarizationMode%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.agentHistorySummarizationWithPromptCache": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.agentHistorySummarizationWithPromptCache%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.agentHistorySummarizationForceGpt41": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.agentHistorySummarizationForceGpt41%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.useResponsesApiTruncation": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.useResponsesApiTruncation%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.omitBaseAgentInstructions": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.omitBaseAgentInstructions%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.promptFileContextProvider.enabled": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.promptFileContextProvider.enabled%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.tools.defaultToolsGrouped": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.tools.defaultToolsGrouped%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.gpt5AlternativePatch": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.gpt5AlternativePatch%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.inlineEdits.triggerOnEditorChangeAfterSeconds": { "type": [ "number", "null" ], "markdownDescription": "%github.copilot.config.inlineEdits.triggerOnEditorChangeAfterSeconds%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.inlineEdits.nextCursorPrediction.displayLine": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.inlineEdits.nextCursorPrediction.displayLine%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.inlineEdits.nextCursorPrediction.currentFileMaxTokens": { "type": "number", "default": 2000, "markdownDescription": "%github.copilot.config.inlineEdits.nextCursorPrediction.currentFileMaxTokens%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.inlineEdits.renameSymbolSuggestions": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.inlineEdits.renameSymbolSuggestions%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.nextEditSuggestions.preferredModel": { "type": "string", "default": "none", "markdownDescription": "%github.copilot.config.nextEditSuggestions.preferredModel%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.nextEditSuggestions.eagerness": { "type": "string", "default": "auto", "enum": [ "auto", "low", "medium", "high" ], "enumItemLabels": [ "%github.copilot.config.nextEditSuggestions.eagerness.auto.label%", "%github.copilot.config.nextEditSuggestions.eagerness.low.label%", "%github.copilot.config.nextEditSuggestions.eagerness.medium.label%", "%github.copilot.config.nextEditSuggestions.eagerness.high.label%" ], "enumDescriptions": [ "%github.copilot.config.nextEditSuggestions.eagerness.auto%", "%github.copilot.config.nextEditSuggestions.eagerness.low%", "%github.copilot.config.nextEditSuggestions.eagerness.medium%", "%github.copilot.config.nextEditSuggestions.eagerness.high%" ], "markdownDescription": "%github.copilot.config.nextEditSuggestions.eagerness%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.cli.mcp.enabled": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.cli.mcp.enabled%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.cli.branchSupport.enabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.cli.branchSupport.enabled%", "tags": [ "advanced" ] }, "github.copilot.chat.cli.planExitMode.enabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.cli.planExitMode.enabled%", "tags": [ "advanced" ] }, "github.copilot.chat.cli.isolationOption.enabled": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.cli.isolationOption.enabled%", "tags": [ "advanced" ] }, "github.copilot.chat.cli.checkpoints.enabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.cli.checkpoints.enabled%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.cli.sessionController.enabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.cli.sessionController.enabled%", "tags": [ "advanced" ] }, "github.copilot.chat.cli.terminalLinks.enabled": { "type": "boolean", "default": true, "markdownDescription": "%github.copilot.config.cli.terminalLinks.enabled%", "tags": [ "advanced" ] }, "github.copilot.chat.searchSubagent.enabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.searchSubagent.enabled%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.searchSubagent.useAgenticProxy": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.searchSubagent.useAgenticProxy%", "tags": [ "advanced" ] }, "github.copilot.chat.searchSubagent.model": { "type": "string", "default": "", "markdownDescription": "%github.copilot.config.searchSubagent.model%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.searchSubagent.toolCallLimit": { "type": "number", "default": 4, "markdownDescription": "%github.copilot.config.searchSubagent.toolCallLimit%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.agentDebugLog.enabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.chat.agentDebugLog.enabled%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.agentDebugLog.fileLogging.enabled": { "type": "boolean", "default": false, "markdownDescription": "%github.copilot.config.chat.agentDebugLog.fileLogging.enabled%", "tags": [ "advanced", "experimental", "onExp" ] }, "github.copilot.chat.agentDebugLog.fileLogging.flushIntervalMs": { "type": "number", "default": 4000, "minimum": 2000, "markdownDescription": "%github.copilot.config.chat.agentDebugLog.fileLogging.flushIntervalMs%", "tags": [ "advanced", "experimental" ] }, "github.copilot.chat.otel.enabled": { "type": "boolean", "default": false, "markdownDescription": "Enable OpenTelemetry trace/metric/log emission for Copilot Chat operations. Env var `COPILOT_OTEL_ENABLED` takes precedence.", "tags": [ "advanced" ] }, "github.copilot.chat.otel.exporterType": { "type": "string", "enum": [ "otlp-grpc", "otlp-http", "console", "file" ], "default": "otlp-http", "markdownDescription": "OTel exporter type for Copilot Chat telemetry.", "tags": [ "advanced" ] }, "github.copilot.chat.otel.otlpEndpoint": { "type": "string", "default": "http://localhost:4318", "markdownDescription": "OTLP collector endpoint URL for Copilot Chat OTel data. Env var `OTEL_EXPORTER_OTLP_ENDPOINT` takes precedence.", "tags": [ "advanced" ] }, "github.copilot.chat.otel.captureContent": { "type": "boolean", "default": false, "markdownDescription": "Capture input/output messages, system instructions, and tool definitions in OTel telemetry. **Contains potentially sensitive data.** Env var `COPILOT_OTEL_CAPTURE_CONTENT` takes precedence.", "tags": [ "advanced" ] }, "github.copilot.chat.otel.outfile": { "type": "string", "default": "", "markdownDescription": "File path for file-based OTel exporter output (JSON-lines). When set, overrides exporter type to `file`.", "tags": [ "advanced" ] } } } ], "submenus": [ { "id": "copilot/reviewComment/additionalActions/applyAndNext", "label": "%github.copilot.submenu.reviewComment.applyAndNext.label%" }, { "id": "copilot/reviewComment/additionalActions/discardAndNext", "label": "%github.copilot.submenu.reviewComment.discardAndNext.label%" }, { "id": "copilot/reviewComment/additionalActions/discard", "label": "%github.copilot.submenu.reviewComment.discard.label%" }, { "id": "github.copilot.chat.debug.filter", "label": "Filter", "icon": "$(filter)" }, { "id": "github.copilot.chat.debug.exportAllPromptLogsAsJson", "label": "Export All Logs as JSON", "icon": "$(file-export)" } ], "menus": { "editor/title": [ { "command": "github.copilot.debug.generateInlineEditTests", "when": "resourceScheme == 'ccreq'" }, { "command": "github.copilot.chat.notebook.enableFollowCellExecution", "when": "config.github.copilot.chat.notebook.followCellExecution.enabled && !github.copilot.notebookFollowInSessionEnabled && github.copilot.notebookAgentModeUsage && !config.notebook.globalToolbar", "group": "navigation@10" }, { "command": "github.copilot.chat.notebook.disableFollowCellExecution", "when": "config.github.copilot.chat.notebook.followCellExecution.enabled && github.copilot.notebookFollowInSessionEnabled && github.copilot.notebookAgentModeUsage && !config.notebook.globalToolbar", "group": "navigation@10" }, { "command": "github.copilot.chat.replay", "group": "navigation@9", "when": "resourceFilename === 'benchRun.chatReplay.json'" }, { "command": "github.copilot.chat.showAsChatSession", "group": "navigation@9", "when": "resourceFilename === 'benchRun.chatReplay.json' || resourceFilename === 'chat-export-logs.json'" }, { "command": "github.copilot.chat.copilotCLI.acceptDiff", "group": "navigation@1", "when": "github.copilot.chat.copilotCLI.hasActiveDiff" }, { "command": "github.copilot.chat.copilotCLI.rejectDiff", "group": "navigation@2", "when": "github.copilot.chat.copilotCLI.hasActiveDiff" } ], "editor/title/context": [ { "command": "github.copilot.chat.copilotCLI.addFileReference", "group": "copilot", "when": "github.copilot.chat.copilotCLI.hasSession && !inOutput && resourceScheme != 'vscode-webview' && resourceScheme != 'webview-panel'" } ], "explorer/context": [ { "command": "github.copilot.chat.showAsChatSession", "when": "resourceFilename === 'benchRun.chatReplay.json' || resourceFilename === 'chat-export-logs.json'", "group": "2_copilot@1" }, { "command": "github.copilot.chat.copilotCLI.addFileReference", "group": "copilot", "when": "github.copilot.chat.copilotCLI.hasSession && !explorerResourceIsFolder" } ], "editor/context": [ { "command": "github.copilot.chat.fix", "when": "!github.copilot.interactiveSession.disabled && !editorReadonly && editorSelectionHasDiagnostics", "group": "1_chat@4" }, { "command": "github.copilot.chat.explain", "when": "!github.copilot.interactiveSession.disabled", "group": "1_chat@5" }, { "command": "github.copilot.chat.review", "when": "config.github.copilot.chat.reviewSelection.enabled && !github.copilot.interactiveSession.disabled && resourceScheme != 'vscode-chat-code-block'", "group": "1_chat@6" }, { "command": "github.copilot.chat.copilotCLI.addFileReference", "group": "copilot", "when": "github.copilot.chat.copilotCLI.hasSession && !inOutput && resourceScheme != 'vscode-webview' && resourceScheme != 'webview-panel'" }, { "command": "github.copilot.chat.copilotCLI.addSelection", "group": "copilot", "when": "github.copilot.chat.copilotCLI.hasSession && editorHasSelection && !inOutput && resourceScheme != 'vscode-webview' && resourceScheme != 'webview-panel'" } ], "chat/editor/inlineGutter": [ { "command": "github.copilot.chat.explain", "when": "!github.copilot.interactiveSession.disabled && editor.hasSelection && !inlineChatFileBelongsToChat", "group": "2_chat@2" }, { "command": "github.copilot.chat.review", "when": "!github.copilot.interactiveSession.disabled && editor.hasSelection && config.github.copilot.chat.reviewSelection.enabled && !inlineChatFileBelongsToChat", "group": "2_chat@3" } ], "chat/input/editing/sessionToolbar": [ { "command": "github.copilot.chat.applyCopilotCLIAgentSessionChanges.apply", "when": "chatSessionType == copilotcli && workbenchState != empty && !isSessionsWindow", "group": "navigation@0" }, { "command": "github.copilot.chat.checkoutPullRequestReroute", "when": "chatSessionType == copilot-cloud-agent && !github.vscode-pull-request-github.activated && gitOpenRepositoryCount != 0", "group": "navigation@0" }, { "command": "github.copilot.chat.openPullRequestCopilotCLIAgentSession.openPR", "when": "chatSessionType == copilotcli && isSessionsWindow && sessions.hasOpenPullRequest", "group": "navigation@1" } ], "chat/input/editing/sessionApplyActions": [ { "command": "github.copilot.chat.mergeCopilotCLIAgentSessionChanges.merge", "when": "chatSessionType == copilotcli && isSessionsWindow && !sessions.isMergeBaseBranchProtected && !sessions.hasOpenPullRequest", "group": "merge@1" }, { "command": "github.copilot.chat.mergeCopilotCLIAgentSessionChanges.mergeAndSync", "when": "chatSessionType == copilotcli && isSessionsWindow && !sessions.isMergeBaseBranchProtected && !sessions.hasOpenPullRequest", "group": "merge@2" }, { "command": "github.copilot.chat.createPullRequestCopilotCLIAgentSession.createPR", "when": "chatSessionType == copilotcli && isSessionsWindow && !sessions.hasOpenPullRequest", "group": "pull_request@1" }, { "command": "github.copilot.chat.createDraftPullRequestCopilotCLIAgentSession.createDraftPR", "when": "chatSessionType == copilotcli && isSessionsWindow && !sessions.hasOpenPullRequest", "group": "pull_request@2" }, { "command": "github.copilot.chat.createPullRequestCopilotCLIAgentSession.updatePR", "when": "chatSessionType == copilotcli && isSessionsWindow && sessions.hasOpenPullRequest", "group": "pull_request@1" } ], "chat/contextUsage/actions": [ { "command": "github.copilot.chat.compact" } ], "chat/newSession": [ { "command": "github.copilot.cli.newSession", "group": "4_recommendations@0" } ], "testing/item/result": [ { "command": "github.copilot.tests.fixTestFailure.fromInline", "when": "testResultState == failed && !testResultOutdated", "group": "inline@2" } ], "testing/item/context": [ { "command": "github.copilot.tests.fixTestFailure.fromInline", "when": "testResultState == failed && !testResultOutdated", "group": "inline@2" } ], "commandPalette": [ { "command": "github.copilot.cli.openInCopilotCLI", "when": "false" }, { "command": "github.copilot.debug.extensionState", "when": "false" }, { "command": "github.copilot.cli.sessions.commitToWorktree", "when": "false" }, { "command": "github.copilot.cli.sessions.commitToRepository", "when": "false" }, { "command": "github.copilot.chat.debug.exportSingleTrajectory", "when": "false" }, { "command": "github.copilot.chat.triggerPermissiveSignIn", "when": "false" }, { "command": "github.copilot.interactiveSession.feedback", "when": "github.copilot-chat.activated && !github.copilot.interactiveSession.disabled" }, { "command": "github.copilot.debug.workbenchState", "when": "true" }, { "command": "github.copilot.chat.rerunWithCopilotDebug", "when": "false" }, { "command": "github.copilot.chat.startCopilotDebugCommand", "when": "false" }, { "command": "github.copilot.git.generateCommitMessage", "when": "false" }, { "command": "github.copilot.git.resolveMergeConflicts", "when": "false" }, { "command": "github.copilot.chat.explain", "when": "false" }, { "command": "github.copilot.chat.review", "when": "!github.copilot.interactiveSession.disabled" }, { "command": "github.copilot.chat.review.apply", "when": "false" }, { "command": "github.copilot.chat.review.applyAndNext", "when": "false" }, { "command": "github.copilot.chat.review.discard", "when": "false" }, { "command": "github.copilot.chat.review.discardAndNext", "when": "false" }, { "command": "github.copilot.chat.review.discardAll", "when": "false" }, { "command": "github.copilot.chat.review.stagedChanges", "when": "false" }, { "command": "github.copilot.chat.review.unstagedChanges", "when": "false" }, { "command": "github.copilot.chat.review.changes", "when": "false" }, { "command": "github.copilot.chat.review.stagedFileChange", "when": "false" }, { "command": "github.copilot.chat.review.unstagedFileChange", "when": "false" }, { "command": "github.copilot.chat.review.previous", "when": "false" }, { "command": "github.copilot.chat.review.next", "when": "false" }, { "command": "github.copilot.chat.review.continueInInlineChat", "when": "false" }, { "command": "github.copilot.chat.review.continueInChat", "when": "false" }, { "command": "github.copilot.chat.review.markHelpful", "when": "false" }, { "command": "github.copilot.chat.review.markUnhelpful", "when": "false" }, { "command": "github.copilot.devcontainer.generateDevContainerConfig", "when": "false" }, { "command": "github.copilot.tests.fixTestFailure", "when": "false" }, { "command": "github.copilot.tests.fixTestFailure.fromInline", "when": "false" }, { "command": "github.copilot.search.markHelpful", "when": "false" }, { "command": "github.copilot.search.markUnhelpful", "when": "false" }, { "command": "github.copilot.search.feedback", "when": "false" }, { "command": "github.copilot.chat.debug.showElements", "when": "false" }, { "command": "github.copilot.chat.debug.hideElements", "when": "false" }, { "command": "github.copilot.chat.debug.showTools", "when": "false" }, { "command": "github.copilot.chat.debug.hideTools", "when": "false" }, { "command": "github.copilot.chat.debug.showNesRequests", "when": "false" }, { "command": "github.copilot.chat.debug.hideNesRequests", "when": "false" }, { "command": "github.copilot.chat.debug.showGhostRequests", "when": "false" }, { "command": "github.copilot.chat.debug.hideGhostRequests", "when": "false" }, { "command": "github.copilot.chat.debug.exportLogItem", "when": "false" }, { "command": "github.copilot.chat.debug.exportPromptArchive", "when": "false" }, { "command": "github.copilot.chat.debug.exportPromptLogsAsJson", "when": "false" }, { "command": "github.copilot.chat.debug.exportAllPromptLogsAsJson", "when": "false" }, { "command": "github.copilot.chat.mcp.setup.check", "when": "false" }, { "command": "github.copilot.chat.mcp.setup.validatePackage", "when": "false" }, { "command": "github.copilot.chat.mcp.setup.flow", "when": "false" }, { "command": "github.copilot.chat.debug.showRawRequestBody", "when": "false" }, { "command": "github.copilot.debug.showOutputChannel", "when": "false" }, { "command": "github.copilot.cli.sessions.delete", "when": "false" }, { "command": "github.copilot.cli.sessions.resumeInTerminal", "when": "false" }, { "command": "github.copilot.cli.sessions.rename", "when": "false" }, { "command": "github.copilot.claude.sessions.rename", "when": "false" }, { "command": "github.copilot.cli.sessions.openRepository", "when": "false" }, { "command": "github.copilot.cli.sessions.openWorktreeInNewWindow", "when": "false" }, { "command": "github.copilot.cli.sessions.openWorktreeInTerminal", "when": "false" }, { "command": "github.copilot.cli.sessions.copyWorktreeBranchName", "when": "false" }, { "command": "github.copilot.cloud.sessions.openInBrowser", "when": "false" }, { "command": "github.copilot.cloud.sessions.proxy.closeChatSessionPullRequest", "when": "false" }, { "command": "github.copilot.cloud.sessions.installPRExtension", "when": "false" }, { "command": "github.copilot.chat.applyCopilotCLIAgentSessionChanges", "when": "false" }, { "command": "github.copilot.chat.applyCopilotCLIAgentSessionChanges.apply", "when": "false" }, { "command": "github.copilot.chat.mergeCopilotCLIAgentSessionChanges.merge", "when": "false" }, { "command": "github.copilot.chat.mergeCopilotCLIAgentSessionChanges.mergeAndSync", "when": "false" }, { "command": "github.copilot.chat.updateCopilotCLIAgentSessionChanges.update", "when": "false" }, { "command": "github.copilot.chat.createPullRequestCopilotCLIAgentSession.createPR", "when": "false" }, { "command": "github.copilot.chat.createPullRequestCopilotCLIAgentSession.updatePR", "when": "false" }, { "command": "github.copilot.chat.createDraftPullRequestCopilotCLIAgentSession.createDraftPR", "when": "false" }, { "command": "github.copilot.chat.openPullRequestCopilotCLIAgentSession.openPR", "when": "false" }, { "command": "github.copilot.chat.showAsChatSession", "when": "false" }, { "command": "github.copilot.chat.checkoutPullRequestReroute", "when": "false" }, { "command": "github.copilot.chat.cloudSessions.openRepository", "when": "false" }, { "command": "github.copilot.nes.captureExpected.start", "when": "github.copilot.inlineEditsEnabled" }, { "command": "github.copilot.nes.captureExpected.submit", "when": "github.copilot.inlineEditsEnabled" }, { "command": "github.copilot.chat.tools.memory.showMemories", "when": "config.github.copilot.chat.tools.memory.enabled" }, { "command": "github.copilot.chat.tools.memory.clearMemories", "when": "config.github.copilot.chat.tools.memory.enabled" } ], "view/title": [ { "submenu": "github.copilot.chat.debug.filter", "when": "view == copilot-chat", "group": "navigation" }, { "command": "github.copilot.chat.debug.exportAllPromptLogsAsJson", "when": "view == copilot-chat", "group": "export@1" }, { "command": "workbench.action.chat.openAgentDebugPanel", "when": "view == copilot-chat", "group": "3_show@0" }, { "command": "github.copilot.debug.showOutputChannel", "when": "view == copilot-chat", "group": "3_show@1" }, { "command": "github.copilot.debug.showChatLogView", "when": "view == workbench.panel.chat.view.copilot", "group": "3_show" } ], "view/item/context": [ { "command": "github.copilot.chat.debug.showRawRequestBody", "when": "view == copilot-chat && viewItem == request", "group": "export@0" }, { "command": "github.copilot.chat.debug.exportLogItem", "when": "view == copilot-chat && (viewItem == toolcall || viewItem == request)", "group": "export@1" }, { "command": "github.copilot.chat.debug.exportPromptArchive", "when": "view == copilot-chat && viewItem == chatprompt", "group": "export@2" }, { "command": "github.copilot.chat.debug.exportPromptLogsAsJson", "when": "view == copilot-chat && viewItem == chatprompt", "group": "export@3" }, { "command": "github.copilot.chat.debug.exportSingleTrajectory", "when": "view == copilot-chat && viewItem == chatprompt", "group": "export@4" } ], "searchPanel/aiResults/commands": [ { "command": "github.copilot.search.markHelpful", "group": "inline@0", "when": "aiResultsTitle && aiResultsRequested" }, { "command": "github.copilot.search.markUnhelpful", "group": "inline@1", "when": "aiResultsTitle && aiResultsRequested" }, { "command": "github.copilot.search.feedback", "group": "inline@2", "when": "aiResultsTitle && aiResultsRequested && github.copilot.debugReportFeedback" } ], "comments/comment/title": [ { "command": "github.copilot.chat.review.markHelpful", "group": "inline@0", "when": "commentController == github-copilot-review" }, { "command": "github.copilot.chat.review.markUnhelpful", "group": "inline@1", "when": "commentController == github-copilot-review" } ], "commentsView/commentThread/context": [ { "command": "github.copilot.chat.review.apply", "group": "context@1", "when": "commentController == github-copilot-review" }, { "command": "github.copilot.chat.review.discard", "group": "context@2", "when": "commentController == github-copilot-review" }, { "command": "github.copilot.chat.review.discardAll", "group": "context@3", "when": "commentController == github-copilot-review" } ], "comments/commentThread/additionalActions": [ { "submenu": "copilot/reviewComment/additionalActions/applyAndNext", "group": "inline@1", "when": "commentController == github-copilot-review && github.copilot.chat.review.numberOfComments > 1" }, { "command": "github.copilot.chat.review.apply", "group": "inline@1", "when": "commentController == github-copilot-review && github.copilot.chat.review.numberOfComments == 1" }, { "submenu": "copilot/reviewComment/additionalActions/discardAndNext", "group": "inline@2", "when": "commentController == github-copilot-review && github.copilot.chat.review.numberOfComments > 1" }, { "submenu": "copilot/reviewComment/additionalActions/discard", "group": "inline@2", "when": "commentController == github-copilot-review && github.copilot.chat.review.numberOfComments == 1" } ], "copilot/reviewComment/additionalActions/applyAndNext": [ { "command": "github.copilot.chat.review.applyAndNext", "group": "inline@1", "when": "commentController == github-copilot-review" }, { "command": "github.copilot.chat.review.apply", "group": "inline@2", "when": "commentController == github-copilot-review" } ], "copilot/reviewComment/additionalActions/discardAndNext": [ { "command": "github.copilot.chat.review.discardAndNext", "group": "inline@1", "when": "commentController == github-copilot-review" }, { "command": "github.copilot.chat.review.discard", "group": "inline@2", "when": "commentController == github-copilot-review" }, { "command": "github.copilot.chat.review.continueInInlineChat", "group": "inline@3", "when": "commentController == github-copilot-review" } ], "copilot/reviewComment/additionalActions/discard": [ { "command": "github.copilot.chat.review.discard", "group": "inline@2", "when": "commentController == github-copilot-review" }, { "command": "github.copilot.chat.review.continueInInlineChat", "group": "inline@3", "when": "commentController == github-copilot-review" } ], "comments/commentThread/title": [ { "command": "github.copilot.chat.review.previous", "group": "inline@1", "when": "commentController == github-copilot-review" }, { "command": "github.copilot.chat.review.next", "group": "inline@2", "when": "commentController == github-copilot-review" }, { "command": "github.copilot.chat.review.continueInChat", "group": "inline@3", "when": "commentController == github-copilot-review" }, { "command": "github.copilot.chat.review.discardAll", "group": "inline@4", "when": "commentController == github-copilot-review" } ], "scm/title": [ { "command": "github.copilot.chat.review.changes", "group": "navigation", "when": "config.github.copilot.chat.reviewAgent.enabled && github.copilot.chat.reviewDiff.enabled && scmProvider == git && scmProviderRootUri in github.copilot.chat.reviewDiff.enabledRootUris" } ], "scm/sourceControl": [ { "command": "github.copilot.cli.openInCopilotCLI", "group": "3_worktree@1", "when": "scmProvider == git" } ], "scm/resourceGroup/context": [ { "command": "github.copilot.chat.review.stagedChanges", "when": "config.github.copilot.chat.reviewAgent.enabled && github.copilot.chat.reviewDiff.enabled && scmProvider == git && scmResourceGroup == index", "group": "inline@-3" }, { "command": "github.copilot.chat.review.unstagedChanges", "when": "config.github.copilot.chat.reviewAgent.enabled && github.copilot.chat.reviewDiff.enabled && scmProvider == git && scmResourceGroup == workingTree", "group": "inline@-3" } ], "scm/resourceState/context": [ { "command": "github.copilot.git.resolveMergeConflicts", "when": "scmProvider == git && scmResourceGroup == merge && git.activeResourceHasMergeConflicts", "group": "z_chat@1" }, { "command": "github.copilot.chat.review.stagedFileChange", "group": "3_copilot", "when": "config.github.copilot.chat.reviewAgent.enabled && github.copilot.chat.reviewDiff.enabled && scmProvider == git && scmResourceGroup == index" }, { "command": "github.copilot.chat.review.unstagedFileChange", "group": "3_copilot", "when": "config.github.copilot.chat.reviewAgent.enabled && github.copilot.chat.reviewDiff.enabled && scmProvider == git && scmResourceGroup == workingTree" } ], "scm/inputBox": [ { "command": "github.copilot.git.generateCommitMessage", "when": "scmProvider == git" } ], "testing/message/context": [ { "command": "github.copilot.tests.fixTestFailure", "when": "testing.testItemHasUri", "group": "inline@1" } ], "issue/reporter": [ { "command": "github.copilot.report" } ], "github.copilot.chat.debug.filter": [ { "command": "github.copilot.chat.debug.showElements", "when": "github.copilot.chat.debug.elementsHidden", "group": "commands@0" }, { "command": "github.copilot.chat.debug.hideElements", "when": "!github.copilot.chat.debug.elementsHidden", "group": "commands@0" }, { "command": "github.copilot.chat.debug.showTools", "when": "github.copilot.chat.debug.toolsHidden", "group": "commands@1" }, { "command": "github.copilot.chat.debug.hideTools", "when": "!github.copilot.chat.debug.toolsHidden", "group": "commands@1" }, { "command": "github.copilot.chat.debug.showNesRequests", "when": "github.copilot.chat.debug.nesRequestsHidden", "group": "commands@2" }, { "command": "github.copilot.chat.debug.hideNesRequests", "when": "!github.copilot.chat.debug.nesRequestsHidden", "group": "commands@2" }, { "command": "github.copilot.chat.debug.showGhostRequests", "when": "github.copilot.chat.debug.ghostRequestsHidden", "group": "commands@3" }, { "command": "github.copilot.chat.debug.hideGhostRequests", "when": "!github.copilot.chat.debug.ghostRequestsHidden", "group": "commands@3" } ], "notebook/toolbar": [ { "command": "github.copilot.chat.notebook.enableFollowCellExecution", "when": "config.github.copilot.chat.notebook.followCellExecution.enabled && !github.copilot.notebookFollowInSessionEnabled && github.copilot.notebookAgentModeUsage && config.notebook.globalToolbar", "group": "navigation/execute@15" }, { "command": "github.copilot.chat.notebook.disableFollowCellExecution", "when": "config.github.copilot.chat.notebook.followCellExecution.enabled && github.copilot.notebookFollowInSessionEnabled && github.copilot.notebookAgentModeUsage && config.notebook.globalToolbar", "group": "navigation/execute@15" } ], "editor/content": [ { "command": "github.copilot.git.resolveMergeConflicts", "group": "z_chat@1", "when": "config.git.enabled && !git.missing && !isInDiffEditor && !isMergeEditor && resource in git.mergeChanges && git.activeResourceHasMergeConflicts" } ], "multiDiffEditor/content": [ { "command": "github.copilot.chat.applyCopilotCLIAgentSessionChanges", "when": "resourceScheme == copilotcli-worktree-changes && workbenchState != empty && !isSessionsWindow" } ], "chat/chatSessions": [ { "command": "github.copilot.claude.sessions.rename", "when": "chatSessionType == claude-code", "group": "1_edit@4" }, { "command": "github.copilot.cli.sessions.delete", "when": "chatSessionType == copilotcli", "group": "1_edit@10" }, { "command": "github.copilot.cli.sessions.rename", "when": "chatSessionType == copilotcli", "group": "1_edit@4" }, { "command": "github.copilot.cli.sessions.openWorktreeInNewWindow", "when": "chatSessionType == copilotcli", "group": "2_open@1" }, { "command": "github.copilot.cli.sessions.openWorktreeInTerminal", "when": "chatSessionType == copilotcli", "group": "2_open@2" }, { "command": "github.copilot.cli.sessions.copyWorktreeBranchName", "when": "chatSessionType == copilotcli", "group": "2_open@3" }, { "command": "github.copilot.cli.sessions.resumeInTerminal", "when": "chatSessionType == copilotcli", "group": "2_open@4" }, { "command": "github.copilot.chat.applyCopilotCLIAgentSessionChanges", "when": "chatSessionType == copilotcli && workbenchState != empty && !isSessionsWindow", "group": "3_apply@0" }, { "command": "github.copilot.cloud.sessions.openInBrowser", "when": "chatSessionType == copilot-cloud-agent", "group": "navigation@10" }, { "command": "github.copilot.cloud.sessions.proxy.closeChatSessionPullRequest", "when": "chatSessionType == copilot-cloud-agent", "group": "1_edit@10" } ], "chat/multiDiff/context": [ { "command": "github.copilot.cloud.sessions.installPRExtension", "when": "chatSessionType == copilot-cloud-agent && !github.copilot.prExtensionInstalled", "group": "inline@1" } ] }, "icons": { "copilot-logo": { "description": "%github.copilot.icon%", "default": { "fontPath": "assets/copilot.woff", "fontCharacter": "\\0041" } }, "copilot-warning": { "description": "%github.copilot.icon%", "default": { "fontPath": "assets/copilot.woff", "fontCharacter": "\\0042" } }, "copilot-notconnected": { "description": "%github.copilot.icon%", "default": { "fontPath": "assets/copilot.woff", "fontCharacter": "\\0043" } } }, "iconFonts": [ { "id": "copilot-font", "src": [ { "path": "assets/copilot.woff", "format": "woff" } ] } ], "terminalQuickFixes": [ { "id": "copilot-chat.fixWithCopilot", "commandLineMatcher": ".+", "commandExitResult": "error", "outputMatcher": { "anchor": "bottom", "length": 1, "lineMatcher": ".+", "offset": 0 }, "kind": "explain" }, { "id": "copilot-chat.generateCommitMessage", "commandLineMatcher": "git add .+", "commandExitResult": "success", "kind": "explain", "outputMatcher": { "anchor": "bottom", "length": 1, "lineMatcher": ".+", "offset": 0 } }, { "id": "copilot-chat.terminalToDebugging", "commandLineMatcher": ".+", "kind": "explain", "commandExitResult": "error", "outputMatcher": { "anchor": "bottom", "length": 1, "lineMatcher": "", "offset": 0 } }, { "id": "copilot-chat.terminalToDebuggingSuccess", "commandLineMatcher": ".+", "kind": "explain", "commandExitResult": "success", "outputMatcher": { "anchor": "bottom", "length": 1, "lineMatcher": "", "offset": 0 } } ], "languages": [ { "id": "ignore", "filenamePatterns": [ ".copilotignore" ], "aliases": [] }, { "id": "markdown", "extensions": [ ".copilotmd" ] } ], "notebooks": [ { "type": "copilot-chat-replay", "displayName": "Copilot Chat Replay", "selector": [ { "filenamePattern": "*.chatreplay.json" } ] } ], "views": { "copilot-chat": [ { "id": "copilot-chat", "name": "Chat Debug", "icon": "assets/debug-icon.svg", "when": "github.copilot.chat.showLogView" } ], "context-inspector": [ { "id": "context-inspector", "name": "Language Context Inspector", "icon": "$(inspect)", "when": "github.copilot.chat.showContextInspectorView" } ] }, "viewsContainers": { "activitybar": [ { "id": "copilot-chat", "title": "Chat Debug", "icon": "assets/debug-icon.svg" }, { "id": "context-inspector", "title": "Language Context Inspector", "icon": "$(inspect)" } ] }, "configurationDefaults": { "workbench.editorAssociations": { "*.copilotmd": "vscode.markdown.preview.editor" } }, "keybindings": [ { "command": "github.copilot.chat.copilotCLI.addFileReference", "key": "ctrl+shift+.", "mac": "cmd+shift+.", "when": "github.copilot.chat.copilotCLI.hasSession && editorTextFocus" }, { "command": "github.copilot.chat.rerunWithCopilotDebug", "key": "ctrl+alt+.", "mac": "cmd+alt+.", "when": "github.copilot-chat.activated && terminalShellIntegrationEnabled && terminalFocus && !terminalAltBufferActive" }, { "command": "github.copilot.nes.captureExpected.confirm", "key": "ctrl+enter", "mac": "cmd+enter", "when": "copilotNesCaptureMode && editorTextFocus" }, { "command": "github.copilot.nes.captureExpected.abort", "key": "escape", "when": "copilotNesCaptureMode && editorTextFocus" } ], "walkthroughs": [ { "id": "copilotWelcome", "title": "%github.copilot.walkthrough.title%", "description": "%github.copilot.walkthrough.description%", "when": "!isWeb", "steps": [ { "id": "copilot.setup.signIn", "title": "%github.copilot.walkthrough.setup.signIn.title%", "description": "%github.copilot.walkthrough.setup.signIn.description%", "when": "chatEntitlementSignedOut && !view.workbench.panel.chat.view.copilot.visible && !github.copilot-chat.activated && !github.copilot.offline && !github.copilot.interactiveSession.individual.disabled && !github.copilot.interactiveSession.individual.expired && !github.copilot.interactiveSession.enterprise.disabled && !github.copilot.interactiveSession.contactSupport && !github.copilot.interactiveSession.invalidToken && !github.copilot.interactiveSession.rateLimited && !github.copilot.interactiveSession.gitHubLoginFailed", "media": { "video": { "dark": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace.mp4", "light": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace-light.mp4", "hc": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace-hc.mp4", "hcLight": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace-hclight.mp4" }, "altText": "%github.copilot.walkthrough.panelChat.media.altText%" } }, { "id": "copilot.setup.signInNoAction", "title": "%github.copilot.walkthrough.setup.signIn.title%", "description": "%github.copilot.walkthrough.setup.noAction.description%", "when": "chatEntitlementSignedOut && view.workbench.panel.chat.view.copilot.visible && !github.copilot-chat.activated && !github.copilot.offline && !github.copilot.interactiveSession.individual.disabled && !github.copilot.interactiveSession.individual.expired && !github.copilot.interactiveSession.enterprise.disabled && !github.copilot.interactiveSession.contactSupport && !github.copilot.interactiveSession.invalidToken && !github.copilot.interactiveSession.rateLimited && !github.copilot.interactiveSession.gitHubLoginFailed", "media": { "video": { "dark": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace.mp4", "light": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace-light.mp4", "hc": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace-hc.mp4", "hcLight": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace-hclight.mp4" }, "altText": "%github.copilot.walkthrough.panelChat.media.altText%" } }, { "id": "copilot.setup.signUp", "title": "%github.copilot.walkthrough.setup.signUp.title%", "description": "%github.copilot.walkthrough.setup.signUp.description%", "when": "chatPlanCanSignUp && !view.workbench.panel.chat.view.copilot.visible && !github.copilot-chat.activated && !github.copilot.offline && (github.copilot.interactiveSession.individual.disabled || github.copilot.interactiveSession.individual.expired) && !github.copilot.interactiveSession.enterprise.disabled && !github.copilot.interactiveSession.contactSupport && !github.copilot.interactiveSession.invalidToken && !github.copilot.interactiveSession.rateLimited && !github.copilot.interactiveSession.gitHubLoginFailed", "media": { "video": { "dark": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace.mp4", "light": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace-light.mp4", "hc": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace-hc.mp4", "hcLight": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace-hclight.mp4" }, "altText": "%github.copilot.walkthrough.panelChat.media.altText%" } }, { "id": "copilot.setup.signUpNoAction", "title": "%github.copilot.walkthrough.setup.signUp.title%", "description": "%github.copilot.walkthrough.setup.noAction.description%", "when": "chatPlanCanSignUp && view.workbench.panel.chat.view.copilot.visible && !github.copilot-chat.activated && !github.copilot.offline && (github.copilot.interactiveSession.individual.disabled || github.copilot.interactiveSession.individual.expired) && !github.copilot.interactiveSession.enterprise.disabled && !github.copilot.interactiveSession.contactSupport && !github.copilot.interactiveSession.invalidToken && !github.copilot.interactiveSession.rateLimited && !github.copilot.interactiveSession.gitHubLoginFailed", "media": { "video": { "dark": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace.mp4", "light": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace-light.mp4", "hc": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace-hc.mp4", "hcLight": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace-hclight.mp4" }, "altText": "%github.copilot.walkthrough.panelChat.media.altText%" } }, { "id": "copilot.panelChat", "title": "%github.copilot.walkthrough.panelChat.title%", "description": "%github.copilot.walkthrough.panelChat.description%", "when": "!chatEntitlementSignedOut || chatIsEnabled ", "media": { "video": { "dark": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace.mp4", "light": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace-light.mp4", "hc": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace-hc.mp4", "hcLight": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/workspace-hclight.mp4" }, "altText": "%github.copilot.walkthrough.panelChat.media.altText%" } }, { "id": "copilot.edits", "title": "%github.copilot.walkthrough.edits.title%", "description": "%github.copilot.walkthrough.edits.description%", "when": "!chatEntitlementSignedOut || chatIsEnabled ", "media": { "video": { "dark": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/edits.mp4", "light": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/edits-light.mp4", "hc": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/edits-hc.mp4", "hcLight": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/edits-hclight.mp4" }, "altText": "%github.copilot.walkthrough.edits.media.altText%" } }, { "id": "copilot.firstSuggest", "title": "%github.copilot.walkthrough.firstSuggest.title%", "description": "%github.copilot.walkthrough.firstSuggest.description%", "when": "!chatEntitlementSignedOut || chatIsEnabled ", "media": { "video": { "dark": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/ghost-text.mp4", "light": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/ghost-text-light.mp4", "hc": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/ghost-text-hc.mp4", "hcLight": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/ghost-text-hclight.mp4" }, "altText": "%github.copilot.walkthrough.firstSuggest.media.altText%" } }, { "id": "copilot.inlineChatNotMac", "title": "%github.copilot.walkthrough.inlineChatNotMac.title%", "description": "%github.copilot.walkthrough.inlineChatNotMac.description%", "when": "!isMac && (!chatEntitlementSignedOut || chatIsEnabled )", "media": { "video": { "dark": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/inline.mp4", "light": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/inline-light.mp4", "hc": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/inline-hc.mp4", "hcLight": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/inline-hclight.mp4" }, "altText": "%github.copilot.walkthrough.inlineChatNotMac.media.altText%" } }, { "id": "copilot.inlineChatMac", "title": "%github.copilot.walkthrough.inlineChatMac.title%", "description": "%github.copilot.walkthrough.inlineChatMac.description%", "when": "isMac && (!chatEntitlementSignedOut || chatIsEnabled )", "media": { "video": { "dark": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/inline.mp4", "light": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/inline-light.mp4", "hc": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/inline-hc.mp4", "hcLight": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/inline-hclight.mp4" }, "altText": "%github.copilot.walkthrough.inlineChatMac.media.altText%" } }, { "id": "copilot.sparkle", "title": "%github.copilot.walkthrough.sparkle.title%", "description": "%github.copilot.walkthrough.sparkle.description%", "when": "!chatEntitlementSignedOut || chatIsEnabled", "media": { "video": { "dark": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/git-commit.mp4", "light": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/git-commit-light.mp4", "hc": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/git-commit-hc.mp4", "hcLight": "https://vscodewalkthroughs.z1.web.core.windows.net/v0.26/git-commit-hclight.mp4" }, "altText": "%github.copilot.walkthrough.sparkle.media.altText%" } } ] } ], "jsonValidation": [ { "fileMatch": "settings.json", "url": "ccsettings://root/schema.json" } ], "typescriptServerPlugins": [ { "name": "@vscode/copilot-typescript-server-plugin", "enableForWorkspaceTypeScriptVersions": true } ], "chatSessions": [ { "type": "claude-code", "name": "claude", "displayName": "Claude", "icon": "$(claude)", "welcomeTitle": "Claude Agent", "welcomeMessage": "Powered by the same agent as Claude Code", "inputPlaceholder": "Run local tasks with Claude, type `#` for adding context", "order": 3, "description": "%github.copilot.session.providerDescription.claude%", "when": "config.github.copilot.chat.claudeAgent.enabled", "canDelegate": true, "requiresCustomModels": true, "capabilities": { "supportsFileAttachments": true, "supportsImageAttachments": true }, "commands": [ { "name": "init", "description": "Initialize a new CLAUDE.md file with codebase documentation" }, { "name": "pr-comments", "description": "Get comments from a GitHub pull request" }, { "name": "review", "description": "Review a pull request" }, { "name": "security-review", "description": "Complete a security review of the pending changes on the current branch" }, { "name": "simplify", "description": "Review changed code for reuse, quality, and efficiency" }, { "name": "loop", "description": "Run a prompt or slash command on a recurring interval" }, { "name": "claude-api", "description": "Help building with Claude API or Anthropic SDK" }, { "name": "agents", "description": "Create and manage specialized Claude agents" }, { "name": "hooks", "description": "Configure Claude Code hooks for tool execution and events" }, { "name": "memory", "description": "Open memory files (CLAUDE.md) for editing" }, { "name": "compact", "description": "Compact the conversation history to save context tokens" } ] }, { "type": "copilotcli", "name": "cli", "displayName": "Copilot CLI", "icon": "$(worktree)", "welcomeTitle": "Copilot CLI", "welcomeMessage": "Run tasks in the background with the Copilot CLI", "inputPlaceholder": "Run tasks in the background with the Copilot CLI, type `#` for adding context", "order": 1, "canDelegate": true, "description": "%github.copilot.session.providerDescription.background%", "when": "config.github.copilot.chat.backgroundAgent.enabled", "capabilities": { "supportsFileAttachments": true, "supportsProblemAttachments": true, "supportsToolAttachments": false, "supportsImageAttachments": true, "supportsSymbolAttachments": true, "supportsSearchResultAttachments": true, "supportsSourceControlAttachments": true, "supportsPromptAttachments": true, "supportsHandOffs": true }, "commands": [ { "name": "delegate", "description": "Delegate chat session to cloud agent and create associated PR", "when": "config.github.copilot.chat.cloudAgent.enabled" }, { "name": "compact", "description": "%github.copilot.command.cli.compact.description%" } ], "customAgentTarget": "github-copilot", "requiresCustomModels": true, "autoAttachReferences": true }, { "type": "copilot-cloud-agent", "alternativeIds": [ "copilot-swe-agent" ], "name": "cloud", "displayName": "Cloud", "icon": "$(cloud)", "welcomeTitle": "Cloud Agent", "welcomeMessage": "Delegate tasks to the cloud", "inputPlaceholder": "Delegate tasks to the cloud, type `#` for adding context", "order": 2, "canDelegate": true, "description": "%github.copilot.session.providerDescription.cloud%", "when": "config.github.copilot.chat.cloudAgent.enabled", "capabilities": { "supportsFileAttachments": true }, "autoAttachReferences": true } ], "debuggers": [ { "type": "vscode-chat-replay", "label": "vscode-chat-replay", "languages": [ "json" ], "when": "resourceFilename === 'benchRun.chatReplay.json'", "configurationAttributes": { "launch": { "properties": { "program": { "type": "string", "description": "Chat replay file to debug (parse for headers)", "default": "${file}" }, "stopOnEntry": { "type": "boolean", "default": true, "description": "Break immediately to step through manually." } }, "required": [ "program" ] } } } ], "chatAgents": [], "chatPromptFiles": [ { "path": "./assets/prompts/plan.prompt.md" }, { "path": "./assets/prompts/init.prompt.md" }, { "path": "./assets/prompts/create-prompt.prompt.md" }, { "path": "./assets/prompts/create-instructions.prompt.md" }, { "path": "./assets/prompts/create-skill.prompt.md" }, { "path": "./assets/prompts/create-agent.prompt.md" }, { "path": "./assets/prompts/create-hook.prompt.md" } ], "chatSkills": [ { "path": "./assets/prompts/skills/project-setup-info-local/SKILL.md", "when": "config.github.copilot.chat.projectSetupInfoSkill.enabled && !config.github.copilot.chat.newWorkspace.useContext7" }, { "path": "./assets/prompts/skills/project-setup-info-context7/SKILL.md", "when": "config.github.copilot.chat.projectSetupInfoSkill.enabled && config.github.copilot.chat.newWorkspace.useContext7" }, { "path": "./assets/prompts/skills/install-vscode-extension/SKILL.md", "when": "config.github.copilot.chat.installExtensionSkill.enabled && config.github.copilot.chat.newWorkspaceCreation.enabled" }, { "path": "./assets/prompts/skills/get-search-view-results/SKILL.md", "when": "config.github.copilot.chat.getSearchViewResultsSkill.enabled" } ], "terminal": { "profiles": [ { "icon": "copilot", "id": "copilot-cli", "title": "GitHub Copilot CLI", "titleTemplate": "${sequence}" } ] } }, "prettier": { "useTabs": true, "tabWidth": 4, "singleQuote": true }, "scripts": { "postinstall": "tsx ./script/postinstall.ts", "prepare": "husky", "vscode-dts:update": "node script/build/vscodeDtsUpdate.js", "vscode-dts:check": "node script/build/vscodeDtsCheck.js", "vscode-dts:dev": "node node_modules/@vscode/dts/index.js dev && node script/build/moveProposedDts.js", "vscode-dts:main": "node node_modules/@vscode/dts/index.js main && node script/build/moveProposedDts.js", "build": "node .esbuild.ts --sourcemaps", "compile": "node .esbuild.ts --dev", "watch": "npm-run-all -p watch:*", "watch:esbuild": "node .esbuild.ts --watch --dev", "watch:tsc-extension": "tsc --noEmit --watch --project tsconfig.json", "watch:tsc-extension-web": "tsc --noEmit --watch --project tsconfig.worker.json", "watch:tsc-simulation-workbench": "tsc --noEmit --watch --project test/simulation/workbench/tsconfig.json", "typecheck": "tsc --noEmit --project tsconfig.json && tsc --noEmit --project test/simulation/workbench/tsconfig.json && tsc --noEmit --project tsconfig.worker.json && tsc --noEmit --project src/extension/completions-core/vscode-node/extension/src/copilotPanel/webView/tsconfig.json", "lint": "eslint . --max-warnings=0", "lint-staged": "eslint --max-warnings=0", "tsfmt": "npx tsfmt -r --verify", "test": "npm-run-all test:*", "test:extension": "vscode-test", "test:sanity": "vscode-test --sanity", "test:unit": "vitest --run --pool=forks", "vitest": "vitest", "bench": "vitest bench", "get_env": "tsx script/setup/getEnv.mts", "get_token": "tsx script/setup/getToken.mts", "prettier": "prettier --list-different --write --cache .", "simulate": "node dist/simulationMain.js", "simulate-require-cache": "node dist/simulationMain.js --require-cache", "simulate-ci": "node dist/simulationMain.js --ci --require-cache", "simulate-update-baseline": "node dist/simulationMain.js --update-baseline", "simulate-gc": "node dist/simulationMain.js --require-cache --gc", "setup": "npm run get_env && npm run get_token", "setup:dotnet": "run-script-os", "setup:dotnet:darwin:linux": "curl -O https://raw.githubusercontent.com/dotnet/install-scripts/main/src/dotnet-install.sh && chmod u+x dotnet-install.sh && ./dotnet-install.sh --channel 10.0 && rm dotnet-install.sh", "setup:dotnet:win32": "powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"Invoke-WebRequest -Uri https://raw.githubusercontent.com/dotnet/install-scripts/main/src/dotnet-install.ps1 -OutFile dotnet-install.ps1; ./dotnet-install.ps1 -channel 10.0; Remove-Item dotnet-install.ps1\"", "analyze-edits": "tsx script/analyzeEdits.ts", "extract-chat-lib": "tsx script/build/extractChatLib.ts", "create_venv": "tsx script/setup/createVenv.mts", "package": "vsce package", "web": "vscode-test-web --headless --extensionDevelopmentPath=. .", "test:prompt": "mocha \"src/extension/completions-core/vscode-node/prompt/**/test/**/*.test.{ts,tsx}\"", "test:completions-core": "tsx src/extension/completions-core/vscode-node/extension/test/runTest.ts" }, "devDependencies": { "@azure/identity": "4.9.1", "@azure/keyvault-secrets": "^4.10.0", "@azure/msal-node": "^3.6.3", "@c4312/scip": "^0.1.0", "@fluentui/react-components": "^9.66.6", "@fluentui/react-icons": "^2.0.305", "@hediet/node-reload": "^0.8.0", "@keyv/sqlite": "^4.0.5", "@octokit/types": "^14.1.0", "@parcel/watcher": "^2.5.1", "@stylistic/eslint-plugin": "^3.0.1", "@types/eslint": "^9.0.0", "@types/express": "^5.0.6", "@types/google-protobuf": "^3.15.12", "@types/js-yaml": "^4.0.9", "@types/markdown-it": "^14.0.0", "@types/minimist": "^1.2.5", "@types/mocha": "^10.0.10", "@types/node": "^22.16.3", "@types/picomatch": "^4.0.0", "@types/react": "17.0.44", "@types/react-dom": "^18.2.17", "@types/sinon": "^17.0.4", "@types/source-map-support": "^0.5.10", "@types/tar": "^6.1.13", "@types/vinyl": "^2.0.12", "@types/vscode": "^1.109.0", "@types/vscode-webview": "^1.57.4", "@types/yargs": "^17.0.24", "@typescript-eslint/eslint-plugin": "^8.35.0", "@typescript-eslint/parser": "^8.32.0", "@typescript-eslint/typescript-estree": "^8.26.1", "@vitest/coverage-v8": "^3.2.4", "@vitest/snapshot": "^1.5.0", "@vscode/debugadapter": "^1.68.0", "@vscode/debugprotocol": "^1.68.0", "@vscode/dts": "^0.4.1", "@vscode/lsif-language-service": "^0.1.0-pre.4", "@vscode/test-cli": "^0.0.11", "@vscode/test-electron": "^2.5.2", "@vscode/test-web": "^0.0.71", "@vscode/vsce": "3.6.0", "agent-browser": "^0.16.3", "copyfiles": "^2.4.1", "csv-parse": "^6.0.0", "dotenv": "^17.2.0", "electron": "^37.2.1", "esbuild": "^0.25.6", "eslint": "^9.30.0", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-header": "^3.1.1", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsdoc": "^51.3.4", "eslint-plugin-no-only-tests": "^3.3.0", "fastq": "^1.19.1", "glob": "^11.1.0", "husky": "^9.1.7", "js-yaml": "^4.1.1", "keyv": "^5.3.2", "lint-staged": "15.2.9", "minimist": "^1.2.8", "mobx": "^6.13.7", "mobx-react-lite": "^4.1.0", "mocha": "^11.7.1", "mocha-junit-reporter": "^2.2.1", "mocha-multi-reporters": "^1.5.1", "monaco-editor": "0.44.0", "npm-run-all": "^4.1.5", "open": "^10.1.2", "openai": "^6.7.0", "outdent": "^0.8.0", "picomatch": "^4.0.2", "playwright": "^1.58.2", "prettier": "^3.6.2", "react": "^17.0.2", "react-dom": "17.0.2", "rimraf": "^6.0.1", "run-script-os": "^1.1.6", "shiki": "~1.15.0", "sinon": "^21.0.0", "source-map-support": "^0.5.21", "tar": "^7.5.11", "ts-dedent": "^2.2.0", "tsx": "^4.20.3", "typescript": "^5.8.3", "typescript-eslint": "^8.36.0", "typescript-formatter": "github:jrieken/typescript-formatter#497efb26bc40b5fa59a350e6eab17bce650a7e4b", "vite-plugin-top-level-await": "^1.5.0", "vite-plugin-wasm": "^3.5.0", "vitest": "^3.0.5", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", "yaml": "^2.8.0", "yargs": "^17.7.2", "zod": "3.25.76" }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.76", "@anthropic-ai/sdk": "^0.79.0", "@github/blackbird-external-ingest-utils": "^0.3.0", "@github/copilot": "^1.0.9", "@google/genai": "^1.22.0", "@humanwhocodes/gitignore-to-minimatch": "1.0.2", "@microsoft/tiktokenizer": "^1.0.10", "@modelcontextprotocol/sdk": "^1.25.2", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.212.0", "@opentelemetry/exporter-logs-otlp-grpc": "^0.212.0", "@opentelemetry/exporter-logs-otlp-http": "^0.212.0", "@opentelemetry/exporter-metrics-otlp-grpc": "^0.212.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.212.0", "@opentelemetry/exporter-trace-otlp-grpc": "^0.212.0", "@opentelemetry/exporter-trace-otlp-http": "^0.212.0", "@opentelemetry/resources": "^2.5.1", "@opentelemetry/sdk-logs": "^0.212.0", "@opentelemetry/sdk-metrics": "^2.5.1", "@opentelemetry/sdk-trace-node": "^2.5.1", "@opentelemetry/semantic-conventions": "^1.39.0", "@sinclair/typebox": "^0.34.41", "@vscode/copilot-api": "^0.2.18", "@vscode/extension-telemetry": "^1.5.1", "@vscode/l10n": "^0.0.18", "@vscode/prompt-tsx": "^0.4.0-alpha.8", "@vscode/tree-sitter-wasm": "0.0.5-php.2", "@vscode/webview-ui-toolkit": "^1.3.1", "@xterm/headless": "^5.5.0", "ajv": "^8.18.0", "applicationinsights": "^2.9.7", "best-effort-json-parser": "^1.2.1", "diff": "^8.0.3", "dompurify": "^3.3.2", "express": "^5.2.1", "ignore": "^7.0.5", "isbinaryfile": "^5.0.4", "jsonc-parser": "^3.3.1", "lru-cache": "^11.1.0", "markdown-it": "^14.1.1", "minimatch": "^10.2.1", "undici": "^7.24.1", "vscode-tas-client": "^0.1.84", "web-tree-sitter": "^0.23.0" }, "overrides": { "@aminya/node-gyp-build": "npm:node-gyp-build@4.8.1", "string_decoder": "npm:string_decoder@1.2.0", "node-gyp": "npm:node-gyp@10.3.1", "zod": "3.25.76" }, "vscodeCommit": "631a8bb1fd9d262e926340015fb4eef442758c0a" } ================================================ FILE: package.nls.json ================================================ { "github.copilot.badge.signUp": "Sign up for GitHub Copilot", "github.copilot.badge.star": "Star Copilot on GitHub", "github.copilot.badge.youtube": "Check out GitHub on Youtube", "github.copilot.badge.twitter": "Follow GitHub on Twitter", "github.copilot.icon": "GitHub Copilot icon", "github.copilot.command.enableEditTracing": "Enable Chat Edit Tracing", "github.copilot.command.disableEditTracing": "Disable Chat Edit Tracing", "github.copilot.command.compactConversation": "Compact Conversation", "github.copilot.command.explainThis": "Explain", "github.copilot.command.reviewAndComment": "Review", "github.copilot.command.applyReviewSuggestion": "Apply", "github.copilot.command.applyReviewSuggestionAndNext": "Apply and Go to Next", "github.copilot.command.discardReviewSuggestion": "Discard", "github.copilot.command.discardReviewSuggestionAndNext": "Discard and Go to Next", "github.copilot.command.discardAllReviewSuggestion": "Discard All", "github.copilot.command.reviewStagedChanges": "Code Review - Staged Changes", "github.copilot.command.reviewUnstagedChanges": "Code Review - Unstaged Changes", "github.copilot.command.reviewChanges": "Code Review - Uncommitted Changes", "github.copilot.command.reviewFileChange": "Review Changes", "github.copilot.command.codeReviewRun": "Run Code Review", "github.copilot.command.gotoPreviousReviewSuggestion": "Previous Suggestion", "github.copilot.command.gotoNextReviewSuggestion": "Next Suggestion", "github.copilot.command.continueReviewInInlineChat": "Discard and Copy to Inline Chat", "github.copilot.command.continueReviewInChat": "View in Chat Panel", "github.copilot.command.helpfulReviewSuggestion": "Helpful", "github.copilot.command.unhelpfulReviewSuggestion": "Unhelpful", "github.copilot.command.fixThis": "Fix", "github.copilot.command.generateThis": "Generate This", "github.copilot.command.openUserPreferences": "Open User Preferences", "github.copilot.command.openMemoryFolder": "Open Memory Folder", "github.copilot.command.sendChatFeedback": "Send Chat Feedback", "github.copilot.command.buildLocalWorkspaceIndex": "Build Local Workspace Index", "github.copilot.command.buildRemoteWorkspaceIndex": "Build Remote Workspace Index", "github.copilot.command.deleteExternalIngestWorkspaceIndex": "Delete External Ingest Workspace Index", "github.copilot.viewsWelcome.individual.expired": "Your Copilot subscription has expired.\n\n[Review Copilot Settings](https://github.com/settings/copilot?editor=vscode)", "github.copilot.viewsWelcome.enterprise": "Contact your GitHub organization administrator to enable Copilot.", "github.copilot.viewsWelcome.offline": { "message": "GitHub Copilot servers could not be reached. Please check your internet connection and try again.\n\n[Retry Connection](command:github.copilot.refreshToken)\n\nSee also [Copilot log](command:github.copilot.debug.showOutputChannel.internal) and [run diagnostics](command:github.copilot.debug.collectDiagnostics.internal).", "comment": [ "{Locked='['}", "{Locked='](command:github.copilot.refreshToken)'}", "{Locked='](command:github.copilot.debug.showOutputChannel.internal)'}", "{Locked='](command:github.copilot.debug.collectDiagnostics.internal)'}" ] }, "github.copilot.viewsWelcome.invalidToken": { "message": "Your GitHub token is invalid. Please sign in again to refresh your authentication.\n\n[Sign In](command:workbench.action.chat.triggerSetupForceSignIn)\n\nSee also [Copilot log](command:github.copilot.debug.showOutputChannel.internal) and [run diagnostics](command:github.copilot.debug.collectDiagnostics.internal).", "comment": [ "{Locked='['}", "{Locked='](command:workbench.action.chat.triggerSetupForceSignIn)'}", "{Locked='](command:github.copilot.debug.showOutputChannel.internal)'}", "{Locked='](command:github.copilot.debug.collectDiagnostics.internal)'}" ] }, "github.copilot.viewsWelcome.rateLimited": { "message": "Your account has exceeded GitHub's API rate limit. Please wait a few minutes and try again.\n\n[Retry](command:github.copilot.refreshToken)\n\nSee also [Copilot log](command:github.copilot.debug.showOutputChannel.internal) and [run diagnostics](command:github.copilot.debug.collectDiagnostics.internal).", "comment": [ "{Locked='['}", "{Locked='](command:github.copilot.refreshToken)'}", "{Locked='](command:github.copilot.debug.showOutputChannel.internal)'}", "{Locked='](command:github.copilot.debug.collectDiagnostics.internal)'}" ] }, "github.copilot.viewsWelcome.gitHubLoginFailed": { "message": "GitHub login failed. Please sign in to your GitHub account to use Copilot.\n\n[Sign In](command:workbench.action.chat.triggerSetupForceSignIn)\n\nSee also [Copilot log](command:github.copilot.debug.showOutputChannel.internal) and [run diagnostics](command:github.copilot.debug.collectDiagnostics.internal).", "comment": [ "{Locked='['}", "{Locked='](command:workbench.action.chat.triggerSetupForceSignIn)'}", "{Locked='](command:github.copilot.debug.showOutputChannel.internal)'}", "{Locked='](command:github.copilot.debug.collectDiagnostics.internal)'}" ] }, "github.copilot.viewsWelcome.contactSupport": { "message": "There seems to be a problem with your account. Please contact GitHub support.\n\n[Contact Support](https://support.github.com/?editor=vscode)", "comment": [ "{Locked='['}", "{Locked='](https://support.github.com/?editor=vscode)'}" ] }, "github.copilot.viewsWelcome.chatDisabled": { "message": "GitHub Copilot Chat is currently disabled for your account by an organization administrator. Contact an organization administrator to enable chat.\n\n[Learn More](https://docs.github.com/en/copilot/managing-copilot/managing-github-copilot-in-your-organization/managing-github-copilot-features-in-your-organization/managing-policies-for-copilot-in-your-organization)", "comment": [ "{Locked='['}", "{Locked='](https://docs.github.com/en/copilot/managing-copilot/managing-github-copilot-in-your-organization/managing-github-copilot-features-in-your-organization/managing-policies-for-copilot-in-your-organization)'}" ] }, "github.copilot.viewsWelcome.switchToReleaseChannel": { "message": "The Pre-Release version of the GitHub Copilot Chat extension is not currently supported in the stable version of VS Code. Please switch to the release version for GitHub Copilot Chat or try VS Code Insiders.\n\n[Switch to Release Version and Reload](command:runCommands?%7B%22commands%22%3A%5B%7B%22command%22%3A%22workbench.extensions.action.switchToRelease%22%2C%22args%22%3A%5B%22GitHub.copilot-chat%22%5D%7D%2C%22workbench.action.reloadWindow%22%5D%7D)\n\n[Switch to VS Code Insiders](https://aka.ms/vscode-insiders)", "comment": [ "{Locked='['}", "{Locked='](command:runCommands?%7B%22commands%22%3A%5B%7B%22command%22%3A%22workbench.extensions.action.switchToRelease%22%2C%22args%22%3A%5B%22GitHub.copilot-chat%22%5D%7D%2C%22workbench.action.reloadWindow%22%5D%7D)'}", "{Locked='](https://aka.ms/vscode-insiders)'}" ] }, "github.copilot.viewsWelcome.debug": { "message": "Debug using a [terminal command](command:github.copilot.chat.startCopilotDebugCommand) or in an [interactive chat](command:workbench.action.chat.open?%7B%22query%22%3A%22%40vscode%20%2FstartDebugging%20%22%2C%22isPartialQuery%22%3Atrue%7D).", "comment": [ "{Locked='['}", "{Locked='](command:github.copilot.chat.startCopilotDebugCommand)'}", "{Locked='](command:workbench.action.chat.open?%7B%22query%22%3A%22%40vscode%20%2FstartDebugging%20%22%2C%22isPartialQuery%22%3Atrue%7D)'}" ] }, "github.copilot.command.logWorkbenchState": "Log Workbench State", "github.copilot.command.togglePowerSaveBlocker": "Toggle Power Save Blocker", "github.copilot.command.showChatLogView": "Show Chat Debug View", "github.copilot.command.showOutputChannel": "Show Output Channel", "github.copilot.command.showContextInspectorView": "Inspect Language Context", "github.copilot.command.validateNesRename": "Validate NES Rename", "github.copilot.command.resetVirtualToolGroups": "Reset Virtual Tool Groups", "github.copilot.command.extensionState": "Log Extension State", "github.copilot.command.showMemories": "Show Memory Files", "github.copilot.command.clearMemories": "Clear All Memory Files", "github.copilot.command.applySuggestionWithCopilot": "Apply Suggestion", "github.copilot.command.explainTerminalLastCommand": "Explain Last Terminal Command", "github.copilot.command.collectWorkspaceIndexDiagnostics": "Collect Workspace Index Diagnostics", "github.copilot.command.triggerPermissiveSignIn": "Login to GitHub with Full Permissions", "github.copilot.command.resetCloudAgentWorkspaceConfirmations": "Reset Cloud Agent Workspace Confirmations", "github.copilot.git.generateCommitMessage": "Generate Commit Message", "github.copilot.git.resolveMergeConflicts": "Resolve Conflicts with AI", "github.copilot.devcontainer.generateDevContainerConfig": "Generate Dev Container Configuration", "github.copilot.config.enableCodeActions": "Controls if Copilot commands are shown as Code Actions when available", "github.copilot.config.renameSuggestions.triggerAutomatically": "Controls whether Copilot generates suggestions for renaming", "github.copilot.config.localeOverride": "Specify a locale that Copilot should respond in, e.g. `en` or `fr`. By default, Copilot will respond using VS Code's configured display language locale.", "github.copilot.config.edits.enabled": "Whether to enable the Copilot Edits feature.", "github.copilot.config.codesearch.enabled": "Whether to enable agentic codesearch when using `#codebase`.", "github.copilot.nextEditSuggestions.enabled": "Whether to enable next edit suggestions (NES).\n\nNES can propose a next edit based on your recent changes. [Learn more](https://aka.ms/vscode-nes) about next edit suggestions.", "github.copilot.nextEditSuggestions.extendedRange": "Whether to allow next edit suggestions (NES) to modify code farther away from the cursor position.", "github.copilot.nextEditSuggestions.fixes": "Whether to offer fixes for diagnostics via next edit suggestions (NES).", "github.copilot.nextEditSuggestions.allowWhitespaceOnlyChanges": "Whether to allow whitespace-only changes be proposed by next edit suggestions (NES).", "github.copilot.chat.copilotDebugCommand.enabled": "Whether the `copilot-debug` command is enabled in the terminal.", "github.copilot.config.terminalChatLocation": "Controls where chat queries from the terminal should be opened.", "github.copilot.config.terminalChatLocation.chatView": "Open the chat view.", "github.copilot.config.terminalChatLocation.quickChat": "Open quick chat.", "github.copilot.config.terminalChatLocation.terminal": "Open terminal inline chat", "github.copilot.config.scopeSelection": "Whether to prompt the user to select a specific symbol scope if the user uses `/explain` and the active editor has no selection.", "github.copilot.config.debugTerminalCommands": "Whether to quick fix hints in the debug terminal and the `copilot-debug` command.", "github.copilot.config.additionalReadAccessPaths": "A list of absolute folder paths outside of the workspace that Copilot Chat is allowed to read from without requiring confirmation. Edit operations remain restricted to the workspace.", "github.copilot.config.debugTerminalCommandPatterns": "A list of commands for which the \"Debug Command\" quick fix action should be shown in the debug terminal.", "github.copilot.config.codeGeneration.instructions": "A set of instructions that will be added to Copilot requests that generate code.\nInstructions can come from: \n- a file in the workspace: `{ \"file\": \"fileName\" }`\n- text in natural language: `{ \"text\": \"Use underscore for field names.\" }`\n\nNote: Keep your instructions short and precise. Poor instructions can degrade Copilot's quality and performance.", "github.copilot.config.codeGeneration.instructions.deprecated": "Use instructions files instead. See https://aka.ms/vscode-ghcp-custom-instructions for more information.", "github.copilot.config.codeGeneration.useInstructionFiles": "Controls whether code instructions from `.github/copilot-instructions.md` are added to Copilot requests.\n\nNote: Keep your instructions short and precise. Poor instructions can degrade Copilot's quality and performance. [Learn more](https://aka.ms/github-copilot-custom-instructions) about customizing Copilot.", "github.copilot.config.codeGeneration.instruction.text": "A text instruction that will be added to Copilot requests that generate code. Optionally, you can specify a language for the instruction.", "github.copilot.config.codeGeneration.instruction.file": "A path to a file that will be added to Copilot requests that generate code. Optionally, you can specify a language for the instruction.", "github.copilot.config.testGeneration.instructions": "A set of instructions that will be added to Copilot requests that generate tests.\nInstructions can come from: \n- a file in the workspace: `{ \"file\": \"fileName\" }`\n- text in natural language: `{ \"text\": \"Use underscore for field names.\" }`\n\nNote: Keep your instructions short and precise. Poor instructions can degrade Copilot's quality and performance.", "github.copilot.config.testGeneration.instructions.deprecated": "Use instructions files instead. See https://aka.ms/vscode-ghcp-custom-instructions for more information.", "github.copilot.config.experimental.testGeneration.instruction.text": "A text instruction that will be added to Copilot requests that generate tests. Optionally, you can specify a language for the instruction.", "github.copilot.config.experimental.testGeneration.instruction.file": "A path to a file that will be added to Copilot requests that generate tests. Optionally, you can specify a language for the instruction.", "github.copilot.config.reviewAgent.enabled": "Enables the code review agent.", "github.copilot.config.reviewSelection.enabled": "Enables code review on current selection.", "github.copilot.config.reviewSelection.instructions": "A set of instructions that will be added to Copilot requests that provide code review for the current selection.\nInstructions can come from: \n- a file in the workspace: `{ \"file\": \"fileName\" }`\n- text in natural language: `{ \"text\": \"Use underscore for field names.\" }`\n\nNote: Keep your instructions short and precise. Poor instructions can degrade Copilot's effectiveness.", "github.copilot.config.reviewSelection.instruction.text": "A text instruction that will be added to Copilot requests that provide code review for the current selection. Optionally, you can specify a language for the instruction.", "github.copilot.config.reviewSelection.instruction.file": "A path to a file that will be added to Copilot requests that provide code review for the current selection. Optionally, you can specify a language for the instruction.", "github.copilot.config.commitMessageGeneration.instructions": "A set of instructions that will be added to Copilot requests that generate commit messages.\nInstructions can come from: \n- a file in the workspace: `{ \"file\": \"fileName\" }`\n- text in natural language: `{ \"text\": \"Use conventional commit message format.\" }`\n\nNote: Keep your instructions short and precise. Poor instructions can degrade Copilot's quality and performance.", "github.copilot.config.commitMessageGeneration.instruction.text": "Text instructions that will be added to Copilot requests that generate commit messages.", "github.copilot.config.commitMessageGeneration.instruction.file": "A path to a file with instructions that will be added to Copilot requests that generate commit messages.", "github.copilot.config.pullRequestDescriptionGeneration.instructions": "A set of instructions that will be added to Copilot requests that generate pull request titles and descriptions.\nInstructions can come from: \n- a file in the workspace: `{ \"file\": \"fileName\" }`\n- text in natural language: `{ \"text\": \"Always include a list of key changes.\" }`\n\nNote: Keep your instructions short and precise. Poor instructions can degrade Copilot's quality and performance.", "github.copilot.config.pullRequestDescriptionGeneration.instruction.text": "Text instructions that will be added to Copilot requests that generate pull request titles and descriptions.", "github.copilot.config.pullRequestDescriptionGeneration.instruction.file": "A path to a file with instructions that will be added to Copilot requests that generate pull request titles and descriptions.", "github.copilot.config.notebook.followCellExecution": "Controls whether the currently executing cell is revealed into the viewport upon execution from Copilot.", "github.copilot.config.notebook.enhancedNextEditSuggestions": "Controls whether to use an enhanced approach for generating next edit suggestions in notebook cells.", "github.copilot.config.imageUpload.enabled": "Enables the use of image upload URLs in chat requests instead of raw base64 strings.", "github.copilot.config.setupTests.enabled": "Enables the `/setupTests` intent and prompting in `/tests` generation.", "github.copilot.config.virtualTools.threshold": "This setting defines the tool count over which virtual tools should be used. Virtual tools group similar sets of tools together and they allow the model to activate them on-demand. Certain tool groups will optimistically be pre-activated. We are actively developing this feature and you experience degraded tool calling once the threshold is hit.\n\nMay be set to `0` to disable virtual tools.", "github.copilot.config.alternateGptPrompt.enabled": "Enables an experimental alternate prompt for GPT models instead of the default prompt.", "github.copilot.config.alternateGeminiModelFPrompt.enabled": "Enables an experimental alternate prompt for Gemini Model F instead of the default prompt.", "github.copilot.config.gpt5CodexAlternatePrompt": "Specifies an experimental alternate prompt to use for the GPT-5-Codex model.", "github.copilot.command.fixTestFailure": "Fix Test Failure", "copilot.description": "Ask or edit in context", "copilot.title": "Build with Agent", "copilot.edits.description": "Edit files in your workspace", "copilot.agent.description": "Edit files in your workspace in agent mode", "copilot.agent.compact.description": "Free up context by compacting the conversation history. Optionally include extra instructions for compaction.", "copilot.workspace.explain.description": "Explain how the code in your active editor works", "copilot.workspace.edit.description": "Edit files in your workspace", "copilot.workspace.review.description": "Review the selected code in your active editor", "copilot.workspace.edit.inline.description": "Edit the selected code in your active editor", "copilot.workspace.generate.description": "Generate new code", "copilot.workspace.doc.description": "Add documentation comment for this symbol", "copilot.workspace.tests.description": "Generate unit tests for the selected code", "copilot.workspace.fix.description": "Propose a fix for the problems in the selected code", "copilot.workspace.fix.sampleRequest": "There is a problem in this code. Rewrite the code to show it with the bug fixed.", "copilot.workspace.new.description": "Scaffold code for a new file or project in a workspace", "copilot.workspace.new.sampleRequest": "Create a RESTful API server using typescript", "copilot.workspace.newNotebook.description": "Create a new Jupyter Notebook", "copilot.workspace.newNotebook.sampleRequest": "How do I create a notebook to load data from a csv file?", "copilot.workspace.semanticSearch.description": "Find relevant code to your query", "copilot.workspace.semanticSearch.sampleRequest": "Where is the toolbar code?", "copilot.vscode.description": "Ask questions about VS Code", "copilot.workspaceSymbols.tool.description": "Search for workspace symbols using language services.", "copilot.codebase.tool.description": "Find relevant file chunks, symbols, and other information via semantic search", "copilot.vscode.tool.description": "Use VS Code API references to answer questions about VS Code extension development.", "copilot.testFailure.tool.description": "Include information about the last unit test failure", "copilot.vscode.sampleRequest": "What is the command to open the integrated terminal?", "copilot.vscode.api.description": "Ask about VS Code extension development", "copilot.vscode.api.sampleRequest": "How do I add text to the status bar?", "copilot.vscode.search.description": "Generate query parameters for workspace search", "copilot.vscode.search.sampleRequest": "Search for 'foo' in all files under my 'src' directory", "copilot.vscode.setupTests.description": "Set up tests in your project (Experimental)", "copilot.vscode.setupTests.sampleRequest": "add playwright tests to my project", "copilot.terminal.description": "Ask about commands", "copilot.terminalPanel.description": "Ask how to do something in the terminal", "copilot.terminal.sampleRequest": "How do I view all files within a directory including sub-directories?", "copilot.terminal.explain.description": "Explain something in the terminal", "copilot.terminal.explain.sampleRequest": "Explain the last command", "github.copilot.submenu.copilot.label": "Copilot", "github.copilot.submenu.reviewComment.applyAndNext.label": "Apply and Go to Next", "github.copilot.submenu.reviewComment.discardAndNext.label": "Discard and Go to Next", "github.copilot.submenu.reviewComment.discard.label": "Discard", "github.copilot.config.useProjectTemplates": "Use relevant GitHub projects as starter projects when using `/new`", "github.copilot.chat.attachFile": "Add File to Chat", "github.copilot.chat.attachSelection": "Add Selection to Chat", "github.copilot.command.collectDiagnostics": "Chat Diagnostics", "github.copilot.command.showNodeSystemCertificatesErrors": "Show Node.js System Certificates Errors", "github.copilot.command.inlineEdit.clearCache": "Clear Inline Suggestion Cache", "github.copilot.command.inlineEdit.reportNotebookNESIssue": "Report Notebook Inline Suggestion Issue", "github.copilot.command.showNotebookLog": "Show Chat Log Notebook", "github.copilot.resetAutomaticCommandExecutionPrompt": "Reset Automatic Command Execution Prompt", "github.copilot.command.generateSTest": "Generate STest From Last Chat Request", "github.copilot.command.generateConfiguration": "Generate Debug Configuration", "github.copilot.command.openWalkthrough": "Open Walkthrough", "github.copilot.walkthrough.title": "GitHub Copilot", "github.copilot.walkthrough.description": "Your AI pair programmer to write code faster and smarter", "github.copilot.walkthrough.signIn.title": "Sign in with GitHub", "github.copilot.walkthrough.signIn.description": "To get started with Copilot, sign in with your GitHub account.\nMake sure you're using the correct GitHub account. You can also sign in later using the account menu.\n\n[Sign In](command:github.copilot.signIn)", "github.copilot.walkthrough.signIn.media.altText": "Sign in to GitHub via this walkthrough or VS Code's account menu", "github.copilot.walkthrough.setup.signIn.title": "Sign in to use Copilot for free", "github.copilot.walkthrough.setup.signIn.description": "You can use Copilot to generate code across multiple files, fix errors, ask questions about your code and much more using natural language.\n We now offer [Copilot for free](https://github.com/features/copilot/plans) with your GitHub account.\n\n[Use Copilot for Free](command:workbench.action.chat.triggerSetupForceSignIn)", "github.copilot.walkthrough.setup.signUp.title": "Get started with Copilot for free", "github.copilot.walkthrough.setup.signUp.description": "You can use Copilot to generate code across multiple files, fix errors, ask questions about your code and much more using natural language.\n We now offer [Copilot for free](https://github.com/features/copilot/plans) with your GitHub account.\n\n[Use Copilot for Free](command:workbench.action.chat.triggerSetupForceSignIn)", "github.copilot.walkthrough.setup.noAction.description": "You can use Copilot to generate code across multiple files, fix errors, ask questions about your code and much more using natural language.\n We now offer [Copilot for free](https://github.com/features/copilot/plans) with your GitHub account.", "github.copilot.walkthrough.firstSuggest.title": "AI-suggested inline suggestions", "github.copilot.walkthrough.firstSuggest.description": "As you type in the editor, Copilot suggests code to help you complete what you started.", "github.copilot.walkthrough.firstSuggest.media.altText": "The video shows different Copilot inline suggestions, where Copilot suggests code to help the user complete their code", "github.copilot.walkthrough.panelChat.title": "Chat about your code", "github.copilot.walkthrough.panelChat.description": "Ask Copilot programming questions or get help with your code using **@workspace**.\n Type **@** to see all available chat participants that you can chat with directly, each with their own expertise.\n[Chat with Copilot](command:workbench.action.chat.open?%7B%22mode%22%3A%22ask%22%7D)", "github.copilot.walkthrough.panelChat.media.altText": "The user invokes @workspace in the Chat panel in the secondary sidebar to understand the code base. Copilot retrieves the relevant information and provides a response with links to the files", "github.copilot.walkthrough.inlineChatNotMac.title": "Use natural language in your files", "github.copilot.walkthrough.inlineChatNotMac.description": "Sometimes, it's easier to describe the code you want to write directly within a file.\nPlace your cursor or make a selection and use **``Ctrl+I``** to open **Inline Chat**.", "github.copilot.walkthrough.inlineChatNotMac.media.altText": "Inline Chat view in the editor. The video shows the user invoking the inline chat widget and asking Copilot to make a change in the file using natural language. Copilot then makes the requested change", "github.copilot.walkthrough.inlineChatMac.title": "Use natural language in your files", "github.copilot.walkthrough.inlineChatMac.description": "Sometimes, it's easier to describe the code you want to write directly within a file.\nPlace your cursor or make a selection and use **``Cmd+I``** to open **Inline Chat**.", "github.copilot.walkthrough.inlineChatMac.media.altText": "The video shows the user invoking the inline chat widget and asking Copilot to make a change in the file using natural language. Copilot then makes the requested change", "github.copilot.walkthrough.edits.title": "Make changes using natural language", "github.copilot.walkthrough.edits.description": "Use **Copilot Edits** to select files you want to work with and describe changes you want to make. Copilot applies them directly to your files.\n[Edit with Copilot](command:workbench.action.chat.open?%7B%22mode%22%3A%22edit%22%7D)", "github.copilot.walkthrough.edits.media.altText": "The video shows the user dragging and dropping files into the Copilot Edits input box located in the secondary sidebar. Copilot then updates the file according to the user’s request", "github.copilot.walkthrough.sparkle.title": "Look out for smart actions", "github.copilot.walkthrough.sparkle.description": "Copilot enhances your coding experience with AI-powered smart actions throughout the VS Code interface.\nLook for $(sparkle) icons, such as in the [Source Control view](command:workbench.view.scm), where Copilot generates commit messages and PR descriptions based on code changes.\n\n[Discover Tips and Tricks](https://code.visualstudio.com/docs/copilot/copilot-vscode-features)", "github.copilot.walkthrough.sparkle.media.altText": "The video shows the sparkle icon in the source control input box being clicked, triggering GitHub Copilot to generate a commit message automatically", "github.copilot.chat.completionContext.typescript.mode": "The execution mode of the TypeScript Copilot context provider.", "github.copilot.chat.languageContext.typescript.enabled": "Enables the TypeScript language context provider for inline suggestions", "github.copilot.chat.languageContext.typescript.items": "Controls which kind of items are included in the TypeScript language context provider.", "github.copilot.chat.languageContext.typescript.includeDocumentation": "Controls whether to include documentation comments in the generated code snippets.", "github.copilot.chat.languageContext.typescript.cacheTimeout": "The cache population timeout for the TypeScript language context provider in milliseconds. The default is 500 milliseconds.", "github.copilot.chat.languageContext.fix.typescript.enabled": "Enables the TypeScript language context provider for /fix commands", "github.copilot.chat.languageContext.inline.typescript.enabled": "Enables the TypeScript language context provider for inline chats (both generate and edit)", "github.copilot.command.rerunWithCopilotDebug": "Debug Last Terminal Command", "github.copilot.config.enableUserPreferences": "Enable remembering user preferences in agent mode.", "github.copilot.tools.createAndRunTask.name": "Create and Run Task", "github.copilot.tools.createAndRunTask.description": "Create and run a task in the workspace", "github.copilot.tools.createAndRunTask.userDescription": "Create and run a task in the workspace", "github.copilot.config.newWorkspaceCreation.enabled": "Whether to enable new agentic workspace creation.", "github.copilot.config.installExtensionSkill.enabled": "Whether to enable the install extension skill for Copilot.", "github.copilot.config.projectSetupInfoSkill.enabled": "Whether to enable the project setup info skill for Copilot.", "github.copilot.config.newWorkspace.useContext7": "Whether to use the [Context7](command:github.copilot.mcp.viewContext7) tools to scaffold project for new workspace creation.", "github.copilot.config.editsNewNotebook.enabled": "Whether to enable the new notebook tool in Copilot Edits.", "github.copilot.config.notebook.inlineEditAgent.enabled": "Enable agent-like behavior from the notebook inline chat widget.", "github.copilot.config.summarizeAgentConversationHistory.enabled": "Whether to auto-compact agent conversation history once the context window is filled.", "github.copilot.config.conversationTranscriptLookup.enabled": "When enabled, after conversation history is summarized the model is informed it can look up the full conversation transcript via read_file.", "github.copilot.tools.createNewWorkspace.name": "Create New Workspace", "github.copilot.tools.openEmptyFolder.name": "Open an empty folder as VS Code workspace", "github.copilot.tools.getProjectSetupInfo.name": "Get Project Setup Info", "github.copilot.tools.searchResults.name": "Search View Results", "github.copilot.tools.searchResults.description": "Get the results of the search view", "github.copilot.config.getSearchViewResultsSkill.enabled": "Enable the Search View Results skill and disable the corresponding tool.", "github.copilot.tools.githubRepo.name": "Search GitHub Repository", "github.copilot.tools.githubRepo.userDescription": "Search a GitHub repository for relevant source code snippets. You can specify a repository using `owner/repo`", "github.copilot.config.autoFix": "Automatically fix diagnostics for edited files.", "github.copilot.config.rateLimitAutoSwitchToAuto": "Automatically switch to the Auto model and retry when you hit a per-model rate limit.", "github.copilot.tools.createNewWorkspace.userDescription": "Scaffold a new workspace in VS Code", "copilot.tools.errors.description": "Check errors for a particular file", "copilot.tools.applyPatch.description": "Edit text files in the workspace", "copilot.tools.findTestFiles.description": "For a source code file, find the file that contains the tests. For a test file, find the file that contains the code under test", "copilot.tools.changes.description": "Get diffs of changed files", "copilot.tools.newJupyterNotebook.description": "Create a new Jupyter Notebook", "copilot.tools.editNotebook.description": "Edit a notebook file in the workspace", "copilot.tools.runNotebookCell.description": "Trigger the execution of a cell in a notebook file", "copilot.tools.getNotebookCellOutput.description": "Read the output of a previously executed cell", "copilot.tools.fetchWebPage.description": "Fetch the main content from a web page. You should include the URL of the page you want to fetch.", "copilot.tools.searchCodebase.name": "Codebase", "copilot.tools.searchWorkspaceSymbols.name": "Workspace Symbols", "copilot.tools.getVSCodeAPI.name": "Get VS Code API References", "copilot.tools.findFiles.name": "Find Files", "copilot.tools.findFiles.userDescription": "Find files by name using a glob pattern", "copilot.tools.findTextInFiles.name": "Find Text In Files", "copilot.tools.findTextInFiles.userDescription": "Search for text in files by regular expression", "copilot.tools.applyPatch.name": "Apply Patch", "copilot.tools.readFile.name": "Read File", "copilot.tools.readFile.userDescription": "Read the contents of a file", "copilot.tools.viewImage.name": "View Image", "copilot.tools.viewImage.userDescription": "View the contents of an image file", "github.copilot.config.tools.viewImage.enabled": "Enable the view image tool, which allows the agent to view image files such as png, jpg, jpeg, gif, and webp.", "copilot.tools.listDirectory.name": "List Dir", "copilot.tools.listDirectory.userDescription": "List the contents of a directory", "copilot.tools.getTaskOutput.name": "Get Task Output", "copilot.tools.getErrors.name": "Get Problems", "copilot.tools.readProjectStructure.name": "Project Structure", "copilot.tools.getChangedFiles.name": "Git Changes", "copilot.tools.testFailure.name": "Test Failure", "copilot.tools.createFile.name": "Create File", "copilot.tools.createFile.description": "Create new files", "copilot.tools.insertEdit.name": "Edit File", "copilot.tools.replaceString.name": "Replace String in File", "copilot.tools.multiReplaceString.name": "Multi-Replace String in Files", "copilot.tools.editNotebook.name": "Edit Notebook", "copilot.tools.editNotebook.userDescription": "Edit a notebook file in the workspace", "copilot.tools.runNotebookCell.name": "Run Notebook Cell", "copilot.tools.getNotebookCellOutput.name": "Get Notebook Cell Output", "copilot.tools.fetchWebPage.name": "Fetch Web Page", "copilot.tools.memory.name": "Memory", "copilot.tools.memory.description": "Store facts about the codebase so they can be recalled in future conversations", "copilot.tools.switchAgent.name": "Switch Agent", "copilot.tools.switchAgent.description": "Switch to a different agent mode. Currently only the Plan agent is supported.", "copilot.tools.findTestFiles.name": "Find Test Files", "copilot.tools.createDirectory.name": "Create Directory", "copilot.tools.createDirectory.description": "Create new directories in your workspace", "github.copilot.config.agent.currentEditorContext.enabled": "When enabled, Copilot will include the name of the current active editor in the context for agent mode.", "github.copilot.config.customInstructionsInSystemMessage": "When enabled, custom instructions and mode instructions will be appended to the system message instead of a user message.", "github.copilot.config.organizationCustomAgents.enabled": "When enabled, Copilot will load custom agents defined by your GitHub Organization.", "github.copilot.config.organizationInstructions.enabled": "When enabled, Copilot will load custom instructions defined by your GitHub Organization.", "github.copilot.config.planAgent.additionalTools": "Additional tools to enable for the Plan agent, on top of built-in tools. Use fully-qualified tool names (e.g., `github/issue_read`, `mcp_server/tool_name`).", "github.copilot.config.implementAgent.model": "Override the language model used when starting implementation from the Plan agent's handoff. Use the format `Model Name (vendor)` (e.g., `GPT-5 (copilot)`). Leave empty to use the default model.", "github.copilot.config.askAgent.additionalTools": "Additional tools to enable for the Ask agent, on top of built-in read-only tools. Use fully-qualified tool names (e.g., `github/issue_read`, `mcp_server/tool_name`).", "github.copilot.config.askAgent.model": "Override the language model used by the Ask agent. Leave empty to use the default model.", "github.copilot.config.exploreAgent.model": "Override the language model used by the Explore subagent. Defaults to a fast, small model. Leave empty to use the built-in fallback list.", "copilot.toolSet.editing.description": "Edit files in your workspace", "copilot.toolSet.read.description": "Read files in your workspace", "copilot.toolSet.search.description": "Search files in your workspace", "copilot.toolSet.web.description": "Fetch information from the web", "github.copilot.config.useMessagesApi": "Use the Messages API instead of the Chat Completions API when supported.", "github.copilot.config.anthropic.contextEditing.mode": "Select the context editing mode for Anthropic models. Automatically manages conversation context as it grows, helping optimize costs and stay within context window limits.\n\n- `off`: Context editing is disabled.\n- `clear-thinking`: Clears thinking blocks while preserving tool uses.\n- `clear-tooluse`: Clears tool uses while preserving thinking blocks.\n- `clear-both`: Clears both thinking blocks and tool uses.\n\n**Note**: This is an experimental feature. Context editing may cause additional cache rewrites. Enable with caution.", "github.copilot.config.anthropic.promptOptimization": "Prompt optimization mode for Claude 4.6 models.\n\n- `control`: Uses the current default prompt (no changes).\n- `combined`: Uses a single optimized prompt for both Opus and Sonnet.\n- `split`: Uses separate optimized prompts for Opus (bounded exploration) and Sonnet (full persistence).\n\n**Note**: This is an experimental feature for A/B testing prompt configurations.", "github.copilot.config.anthropic.toolSearchTool.enabled": "Enable tool search tool for Anthropic models. When enabled, tools are dynamically discovered and loaded on-demand using natural language search, reducing context window usage when many tools are available.", "github.copilot.config.anthropic.toolSearchTool.mode": "Controls how tool search works for Anthropic models. 'server' uses Anthropic's built-in regex-based tool search. 'client' uses local embeddings-based semantic search for more accurate tool discovery.", "github.copilot.config.useResponsesApi": "Use the Responses API instead of the Chat Completions API when supported. Enables reasoning and reasoning summaries.\n\n**Note**: This is an experimental feature that is not yet activated for all users.\n\n**Important**: URL API path resolution for custom OpenAI-compatible and Azure models is independent of this setting and fully determined by `url` property of `#github.copilot.chat.customOAIModels#` or `#github.copilot.chat.azureModels#` respectively.", "github.copilot.config.responsesApiReasoningSummary": "Sets the reasoning summary style used for the Responses API. Requires `#github.copilot.chat.useResponsesApi#`.", "github.copilot.config.responsesApiContextManagement.enabled": "Enables context management for the Responses API. Requires `#github.copilot.chat.useResponsesApi#`.", "github.copilot.config.updated53CodexPrompt.enabled": "Enables the updated prompt for gpt-5.3-codex model.", "github.copilot.config.anthropic.thinking.budgetTokens": "Maximum number of tokens to allocate for extended thinking in Anthropic models. Setting this value enables extended thinking. Valid range is `1,024` to `max_tokens-1`.", "github.copilot.config.anthropic.thinking.forceExtendedThinking": "Force extended thinking for models that support adaptive thinking (e.g., Sonnet 4.6, Opus 4.6). When enabled, uses explicit token budgets instead of adaptive thinking.", "github.copilot.config.anthropic.promptCaching.extendedTtl": "Enable extended prompt cache TTL for Anthropic models.", "github.copilot.config.anthropic.tools.websearch.enabled": "Enable Anthropic's native web search tool for BYOK Claude models. When enabled, allows Claude to search the web for current information. \n\n**Note**: This is an experimental feature only available for BYOK Anthropic Claude models.", "github.copilot.config.anthropic.tools.websearch.maxUses": "Maximum number of web searches allowed per request. Valid range is 1 to 20. Prevents excessive API calls within a single interaction. If Claude exceeds this limit, the response returns an error.", "github.copilot.config.anthropic.tools.websearch.allowedDomains": "List of domains to restrict web search results to (e.g., `[\"example.com\", \"docs.example.com\"]`). Domains should not include the HTTP/HTTPS scheme. Subdomains are automatically included. Cannot be used together with blocked domains.", "github.copilot.config.anthropic.tools.websearch.blockedDomains": "List of domains to exclude from web search results (e.g., `[\"untrustedsource.com\"]`). Domains should not include the HTTP/HTTPS scheme. Subdomains are automatically excluded. Cannot be used together with allowed domains.", "github.copilot.config.anthropic.tools.websearch.userLocation": "User location for personalizing web search results based on geographic context. All fields (city, region, country, timezone) are optional. Example: `{\"city\": \"San Francisco\", \"region\": \"California\", \"country\": \"US\", \"timezone\": \"America/Los_Angeles\"}`", "github.copilot.config.switchAgent.enabled": "Allow agent to switch to the Plan agent for research, exploration, and planning tasks.", "github.copilot.config.completionsFetcher": "Sets the fetcher used for the inline completions.", "github.copilot.config.nesFetcher": "Sets the fetcher used for the next edit suggestions.", "github.copilot.config.debug.overrideChatEngine": "Override the chat model. This allows you to test with different models.\n\n**Note**: This is an advanced debugging setting and should not be used while self-hosting as it may lead to a different experience compared to end-users.", "github.copilot.config.projectLabels.expanded": "Use the expanded format for project labels in prompts.", "github.copilot.config.projectLabels.chat": "Add project labels in chat requests.", "github.copilot.config.projectLabels.inline": "Add project labels in inline edit requests.", "github.copilot.config.workspace.maxLocalIndexSize": "Maximum size of the local workspace index.", "github.copilot.config.workspace.enableFullWorkspace": "Enable full workspace context analysis.", "github.copilot.config.workspace.enableCodeSearch": "Enable code search in workspace context.", "github.copilot.config.workspace.enableEmbeddingsSearch": "Enable embeddings-based search in workspace context.", "github.copilot.config.workspace.preferredEmbeddingsModel": "Preferred embeddings model for semantic search.", "github.copilot.config.workspace.prototypeAdoCodeSearchEndpointOverride": "Override endpoint for Azure DevOps code search prototype.", "github.copilot.config.feedback.onChange": "Enable feedback collection on configuration changes.", "github.copilot.config.review.intent": "Enable intent detection for code review.", "github.copilot.config.notebook.summaryExperimentEnabled": "Enable the notebook summary experiment.", "github.copilot.config.notebook.variableFilteringEnabled": "Enable filtering variables by cell document symbols.", "github.copilot.config.notebook.alternativeFormat": "Alternative document format for notebooks.", "github.copilot.config.notebook.alternativeNESFormat.enabled": "Enable alternative format for Next Edit Suggestions in notebooks.", "github.copilot.config.localWorkspaceRecording.enabled": "Enable local workspace recording for analysis.", "github.copilot.config.editRecording.enabled": "Enable edit recording for analysis.", "github.copilot.config.inlineChat.selectionRatioThreshold": "Threshold at which to switch editing strategies for inline chat. When a selection portion of code matches a parse tree node, only that is presented to the language model. This speeds up response times but might have lower quality results. Requires having a parse tree for the document and the `inlineChat.enableV2` setting. Values must be between 0 and 1, where 0 means off and 1 means the selection perfectly matches a parse tree node.", "github.copilot.config.debug.requestLogger.maxEntries": "Maximum number of entries to keep in the request logger for debugging purposes.", "github.copilot.config.chat.agentDebugLog.enabled": "When enabled, collect agent request information (tool calls, LLM requests, token usage, and errors) for viewing and troubleshooting in VS Code. Requires window reload to take effect.", "github.copilot.config.chat.agentDebugLog.fileLogging.enabled": "Enable writing chat debug events to JSONL files on disk for diagnostics. When disabled, the built-in `troubleshoot` skill is also disabled. Requires window reload to take effect.", "github.copilot.config.chat.agentDebugLog.fileLogging.flushIntervalMs": "How often (in milliseconds) buffered debug log entries are flushed to disk. Lower values provide more up-to-date logs at the cost of more frequent disk writes.", "github.copilot.config.inlineEdits.diagnosticsContextProvider.enabled": "Enable diagnostics context provider for next edit suggestions.", "github.copilot.config.inlineEdits.chatSessionContextProvider.enabled": "Enable chat session context provider for next edit suggestions.", "github.copilot.config.codesearch.agent.enabled": "Enable code search capabilities in agent mode.", "github.copilot.config.agent.temperature": "Temperature setting for agent mode requests.", "github.copilot.config.agent.omitFileAttachmentContents": "Omit summarized file contents from file attachments in agent mode, to encourage the agent to properly read and explore.", "github.copilot.config.agent.largeToolResultsToDisk.enabled": "When enabled, large tool results are written to disk instead of being included directly in the context, helping manage context window usage.", "github.copilot.config.agent.largeToolResultsToDisk.thresholdBytes": "The size threshold in bytes above which tool results are written to disk. Only applies when largeToolResultsToDisk.enabled is true.", "github.copilot.config.instantApply.shortContextModelName": "Model name for short context instant apply.", "github.copilot.config.instantApply.shortContextLimit": "Token limit for short context instant apply.", "github.copilot.config.summarizeAgentConversationHistoryThreshold": "Threshold for compacting agent conversation history.", "github.copilot.config.agentHistorySummarizationMode": "Mode for agent history summarization.", "github.copilot.config.backgroundCompaction": "Enable background compaction of conversation history.", "github.copilot.config.agentHistorySummarizationWithPromptCache": "Use prompt caching for agent history summarization.", "github.copilot.config.agentHistorySummarizationForceGpt41": "Force GPT-4.1 for agent history summarization.", "github.copilot.config.useResponsesApiTruncation": "Use Responses API for truncation.", "github.copilot.config.enableReadFileV2": "Enable version 2 of the read file tool.", "github.copilot.config.enableAskAgent": "Enable the Ask agent for answering questions.", "github.copilot.config.omitBaseAgentInstructions": "Omit base agent instructions from prompts.", "github.copilot.config.promptFileContextProvider.enabled": "Enable prompt file context provider.", "github.copilot.config.tools.defaultToolsGrouped": "Group default tools in prompts.", "github.copilot.config.claudeAgent.enabled": "Enable Claude Agent sessions in VS Code. Start and resume agentic coding sessions powered by Anthropic's Claude Agent SDK directly in the editor. Uses your existing Copilot subscription.", "github.copilot.config.claudeAgent.allowDangerouslySkipPermissions": "Allow bypass permissions mode. Recommended only for sandboxes with no internet access.", "github.copilot.config.cli.mcp.enabled": "Enable Model Context Protocol (MCP) server for Background Agents.", "github.copilot.config.cli.branchSupport.enabled": "Enable branch support for Background Agents.", "github.copilot.config.cli.planExitMode.enabled": "Enable Plan Mode exit handling in Copilot CLI.", "github.copilot.config.cli.isolationOption.enabled": "Enable the isolation mode option for Background Agents. When enabled, users can choose between Worktree and Workspace modes.", "github.copilot.config.cli.checkpoints.enabled": "Enable checkpoints for Background Agents. When enabled, users can restore changes to a previous state.", "github.copilot.config.cli.sessionController.enabled": "Enable the new session controller API for Background Agents. Requires VS Code reload.", "github.copilot.config.cli.terminalLinks.enabled": "Enable advanced clickable file links in Copilot CLI terminals. Resolves relative paths against session state directories. Requires VS Code reload.", "github.copilot.config.backgroundAgent.enabled": "Enable the Background Agent. When disabled, the Background Agent will not be available in 'Continue In' context menus.", "github.copilot.config.cloudAgent.enabled": "Enable the Cloud Agent. When disabled, the Cloud Agent will not be available in 'Continue In' context menus.", "github.copilot.config.copilotMemory.enabled": "Enable agentic memory for GitHub Copilot. When enabled, Copilot can store repository-scoped facts about your codebase conventions, structure, and preferences remotely on GitHub, and recall them in future conversations to provide more contextually relevant assistance. [Learn more](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/copilot-memory).", "github.copilot.config.tools.memory.enabled": "Enable the memory tool to let the agent save and recall notes during a conversation. Memories are stored locally in VS Code storage — user-scoped memories persist across workspaces and sessions, while session-scoped memories are cleared when the conversation ends.", "github.copilot.config.gpt5AlternativePatch": "Enable GPT-5 alternative patch format.", "github.copilot.config.inlineEdits.triggerOnEditorChangeAfterSeconds": "Trigger inline edits after editor has been idle for this many seconds.", "github.copilot.config.inlineEdits.nextCursorPrediction.displayLine": "Display predicted cursor line for next edit suggestions.", "github.copilot.config.inlineEdits.nextCursorPrediction.currentFileMaxTokens": "Maximum tokens for current file in next cursor prediction.", "github.copilot.config.inlineEdits.renameSymbolSuggestions": "Enable rename symbol suggestions in inline edits.", "github.copilot.config.nextEditSuggestions.preferredModel": "Preferred model for next edit suggestions.", "github.copilot.config.nextEditSuggestions.eagerness": "Controls how eagerly next edit suggestions are shown. Higher values show more suggestions with less delay.", "github.copilot.config.nextEditSuggestions.eagerness.auto": "Automatically determine the eagerness level.", "github.copilot.config.nextEditSuggestions.eagerness.auto.label": "Auto", "github.copilot.config.nextEditSuggestions.eagerness.low": "Show fewer suggestions with longer delays.", "github.copilot.config.nextEditSuggestions.eagerness.low.label": "Low", "github.copilot.config.nextEditSuggestions.eagerness.medium": "Balanced suggestion frequency and delay.", "github.copilot.config.nextEditSuggestions.eagerness.medium.label": "Medium", "github.copilot.config.nextEditSuggestions.eagerness.high": "Show more suggestions with minimal delay.", "github.copilot.config.nextEditSuggestions.eagerness.high.label": "High", "github.copilot.command.deleteAgentSession": "Delete...", "github.copilot.command.cli.sessions.resumeInTerminal": "Resume in Terminal", "github.copilot.command.cli.sessions.rename": "Rename...", "github.copilot.command.cli.compact.description": "Free up context by compacting the conversation history.", "github.copilot.command.claude.sessions.rename": "Rename...", "github.copilot.command.cli.sessions.openRepository": "Open Repository", "github.copilot.command.cli.sessions.openWorktreeInNewWindow": "Open Worktree in New Window", "github.copilot.command.cli.sessions.openWorktreeInTerminal": "Open Worktree in Terminal", "github.copilot.command.cli.sessions.copyWorktreeBranchName": "Copy Worktree Branch Name", "github.copilot.command.cli.sessions.commitToWorktree": "Commit File to Worktree", "github.copilot.command.cli.sessions.commitToRepository": "Commit File to Repository", "github.copilot.command.cli.newSession": "New Copilot CLI Session", "github.copilot.command.cli.newSessionToSide": "New Copilot CLI Session to the Side", "github.copilot.command.cli.openInCopilotCLI": "Open in GitHub Copilot CLI", "github.copilot.command.chat.copilotCLI.addFileReference": "Add File to Copilot CLI", "github.copilot.command.chat.copilotCLI.addSelection": "Add Selection to Copilot CLI", "github.copilot.command.chat.copilotCLI.acceptDiff": "Accept Changes", "github.copilot.command.chat.copilotCLI.rejectDiff": "Reject Changes", "github.copilot.command.openCopilotAgentSessionsInBrowser": "Open in Browser", "github.copilot.command.closeChatSessionPullRequest.title": "Close Pull Request", "github.copilot.command.installPRExtension.title": "Install GitHub Pull Request Extension", "github.copilot.chat.applyCopilotCLIAgentSessionChanges.apply": "Apply", "github.copilot.chat.mergeCopilotCLIAgentSessionChanges.merge": "Merge Changes", "github.copilot.chat.mergeCopilotCLIAgentSessionChanges.mergeAndSync": "Merge Changes & Sync", "github.copilot.chat.updateCopilotCLIAgentSessionChanges.update": "Update Branch", "github.copilot.chat.createPullRequestCopilotCLIAgentSession.createPR": "Create Pull Request", "github.copilot.chat.createPullRequestCopilotCLIAgentSession.updatePR": "Update Pull Request", "github.copilot.chat.createDraftPullRequestCopilotCLIAgentSession.createDraftPR": "Create Draft Pull Request", "github.copilot.chat.openPullRequestCopilotCLIAgentSession.openPR": "Open Pull Request", "github.copilot.command.checkoutPullRequestReroute.title": "Checkout", "github.copilot.command.cloudSessions.openRepository.title": "Browse repositories...", "github.copilot.command.cloudSessions.clearCaches.title": "Clear Cloud Agent Caches", "github.copilot.command.applyCopilotCLIAgentSessionChanges": "Apply Changes to Workspace", "github.copilot.config.githubMcpServer.enabled": "Enable built-in support for the GitHub MCP Server.", "github.copilot.config.githubMcpServer.toolsets": "Specify toolsets to use from the GitHub MCP Server. [Learn more](https://aka.ms/vscode-gh-mcp-toolsets).", "github.copilot.config.githubMcpServer.readonly": "Enable read-only mode for the GitHub MCP Server. When enabled, only read tools are available. [Learn more](https://aka.ms/vscode-gh-mcp-readonly).", "github.copilot.config.githubMcpServer.lockdown": "Enable lockdown mode for the GitHub MCP Server. When enabled, hides public issue details created by users without push access. [Learn more](https://aka.ms/vscode-gh-mcp-lockdown).", "copilot.tools.runSubagent.name": "Run Subagent", "copilot.tools.runSubagent.description": "Runs a task within an isolated subagent context. Enables efficient organization of tasks and context window management.", "copilot.tools.searchSubagent.name": "Search Subagent", "copilot.tools.searchSubagent.description": "Launch an iterative search-focused subagent to find relevant code in your workspace.", "github.copilot.config.searchSubagent.enabled": "Enable the search subagent tool for iterative code exploration in the workspace.", "github.copilot.config.searchSubagent.useAgenticProxy": "Use the agentic proxy for the search subagent tool.", "github.copilot.config.searchSubagent.model": "Model to use for the search subagent. When useAgenticProxy is enabled, defaults to 'agentic-search-v3'. Otherwise defaults to the main agent model.", "github.copilot.config.searchSubagent.toolCallLimit": "Maximum number of tool calls the search subagent can make during exploration.", "copilot.tools.executionSubagent.name": "Execution Subagent", "copilot.tools.executionSubagent.description": "Launch an execution-focused subagent that runs one or more terminal commands to accomplish a task. It is designed to select an efficient summary of the terminal outputs to return to the main agent context.", "github.copilot.config.executionSubagent.enabled": "Enable the Execution Subagent tool in Copilot Chat. The Execution Subagent is designed to run terminal commands to accomplish an execution-based task.", "github.copilot.config.executionSubagent.model": "The model to use for the Execution Subagent tool in Copilot Chat. Leave empty to use the default model.", "github.copilot.config.executionSubagent.toolCallLimit": "Maximum number of tool calls the Execution Subagent can make during execution.", "github.copilot.session.providerDescription.claude": "Delegate tasks to the Claude SDK running locally on your machine. The agent iterates via chat and works asynchronously to implement changes.", "github.copilot.session.providerDescription.cloud": "Delegate tasks to the GitHub Copilot coding agent. The agent iterates via chat and works asynchronously in the cloud to implement changes and pull requests as needed.", "github.copilot.session.providerDescription.background": "Delegate tasks to a background agent running locally on your machine. The agent iterates via chat and works asynchronously in a Git worktree to implement changes isolated from your main workspace using the GitHub Copilot CLI." } ================================================ FILE: script/alternativeAction/index.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import csvParse from 'csv-parse'; import * as fs from 'fs/promises'; import minimist from 'minimist'; import { IAlternativeAction } from '../../src/extension/inlineEdits/node/nextEditProviderTelemetry'; import { coalesce } from '../../src/util/vs/base/common/arrays'; import { Processor } from './processor'; import { IData, Scoring } from './types'; import { Either, log } from './util'; async function extractFromCsv(csvContents: string): Promise<(Scoring.t | undefined)[]> { const options = { columns: true as const, // Use first row as column headers delimiter: ',', // Comma delimiter quote: '"', // Double quotes escape: '"', // Standard CSV escape character skip_empty_lines: true, // Skip any empty rows trim: true, // Remove whitespace around fields relax_quotes: true, // Handle quotes within fields more flexibly bom: true, // Handle UTF-8 BOM cast: false // Keep all values as strings initially } as const; type CsvRecord = { Data: string }; const objects = (await new Promise((resolve, reject) => csvParse.parse(csvContents, options, (err, result) => { if (err) { reject(err); } else { if (result.every((item: any) => typeof item === 'object' && 'Data' in item && typeof item['Data'] === 'string')) { resolve(result); } else { reject(new Error('Invalid CSV format')); } } }) )).map(record => JSON.parse(record.Data) as IData); const scoredEdits = objects.map((obj: IData) => { const altAction: IAlternativeAction = obj.altAction; if (!altAction || !altAction.recording) { return undefined; } return Processor.createScoringForAlternativeAction(altAction, coalesce([parseSuggestedEdit(obj.postProcessingOutcome.suggestedEdit)]), false); }); return scoredEdits; } function writeFiles(basename: string, scoring: Scoring.t) { return [ fs.writeFile(`${basename}.scoredEdits.w.json`, JSON.stringify(scoring, null, 2)), fs.writeFile(`${basename}.recording.w.json`, JSON.stringify(scoring.scoringContext.recording, null, 2)), ]; } async function handleCsv(inputFilePath: string) { log('Handling CSV file:', inputFilePath); const csvContents = await fs.readFile(inputFilePath, 'utf8'); log('CSV contents read, length:', csvContents.length); const extracted = await extractFromCsv(csvContents); log('Extraction complete, number of scored edits:', extracted.filter(e => e).length); try { await Promise.all(extracted.flatMap((obj: Scoring.t | undefined, idx: number) => { if (!obj) { return []; } return writeFiles(idx.toString(), obj); })); log('All files written successfully'); } catch (e) { log('Error writing files:', e); } } function parseFile(fileContents: string): Either | undefined { let parsedObj: unknown; try { parsedObj = JSON.parse(fileContents); } catch (e) { console.error('Failed to parse JSON:', e); return undefined; } if (parsedObj && typeof parsedObj === 'object' && 'prompt' in parsedObj) { return Either.left(parsedObj as IData); } return Either.right(parsedObj as IAlternativeAction); } async function handleAlternativeActionJson(inputFilePath: string) { log('Handling alternative action JSON file:', inputFilePath); const fileContents = await fs.readFile(inputFilePath, 'utf8'); log('File contents read, length:', fileContents.length); const obj = parseFile(fileContents); if (!obj) { console.error('Failed to parse alternative action JSON file'); return; } const altAction = obj.isLeft() ? obj.value.altAction : obj.value; const edits: [start: number, endEx: number, text: string][] = []; let isAccepted = false; if (obj.isLeft()) { const data = obj.value; const parsedEdit = parseSuggestedEdit(data.postProcessingOutcome.suggestedEdit); if (parsedEdit) { edits.push(parsedEdit); } isAccepted = data.suggestionStatus === 'accepted'; } const scoring = Processor.createScoringForAlternativeAction(altAction, edits, isAccepted); if (!scoring) { console.error('Failed to create scoring from alternative action'); return; } const outputFilePath = inputFilePath.replace(/\.json$/, '.scoredEdits.json'); await Promise.all(writeFiles(outputFilePath.replace(/\.scoredEdits\.json$/, ''), scoring)); log('Scoring written to:', outputFilePath); } function parseSuggestedEdit(suggestedEditStr: string): [number, number, string] | null { const [stringifiedRange, quotedText] = suggestedEditStr.split(' -> '); const match = stringifiedRange.match(/^\[(\d+), (\d+)\)$/); if (match) { const start = parseInt(match[1], 10); const endEx = parseInt(match[2], 10); const text = quotedText.slice(1, -1); // Remove surrounding quotes return [start, endEx, text]; } return null; } async function main() { const argv = minimist(process.argv.slice(2), { alias: { p: 'path', s: 'single', c: 'csv' }, boolean: ['single', 'csv'], string: ['path'] }); if (!argv.path) { console.error('Please provide a path to an alternative action JSON file using --path or -p'); process.exit(1); } const inputFilePath = argv.path; if (argv.csv) { await handleCsv(inputFilePath); return; } await handleAlternativeActionJson(inputFilePath); return; } main(); ================================================ FILE: script/alternativeAction/processor.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IAlternativeAction } from '../../src/extension/inlineEdits/node/nextEditProviderTelemetry'; import { Edits } from '../../src/platform/inlineEdits/common/dataTypes/edit'; import { LogEntry } from '../../src/platform/workspaceRecorder/common/workspaceLog'; import { StringEdit, StringReplacement } from '../../src/util/vs/editor/common/core/edits/stringEdit'; import { OffsetRange } from '../../src/util/vs/editor/common/core/ranges/offsetRange'; import { ISerializedEdit } from '../logRecordingTypes'; import { IStringReplacement, NextUserEdit, Recording, Scoring, SuggestedEdit } from './types'; import { binarySearch, log } from './util'; export namespace Processor { export function createScoringForAlternativeAction( altAction: IAlternativeAction, proposedEdits: IStringReplacement[], isAccepted: boolean, ): Scoring.t | undefined { const processedRecording = splitRecordingAtRequestTime(altAction); if (!processedRecording) { log('Could not split recording at request time'); return undefined; } const { recordingPriorToRequest, recordingAfterRequest } = processedRecording; const currentFileId = determineCurrentFileId(recordingPriorToRequest); if (currentFileId === undefined) { log('Could not determine current file ID from recording prior to request'); return undefined; } const idToFileMap = documentIndexMapping(recordingPriorToRequest); const currentFilePath = idToFileMap.get(currentFileId); if (!currentFilePath) { log('Could not find current file path from ID mapping'); return undefined; } const currentFile = { id: currentFileId, relativePath: currentFilePath }; const nextUserEdit = getNextUserEdit(currentFile, recordingPriorToRequest, recordingAfterRequest); const reconstructedRecording: Recording.t = { log: recordingPriorToRequest, nextUserEdit, }; const nesEdits = proposedEdits.map((se): SuggestedEdit.t => ({ documentUri: currentFile.relativePath, edit: [se], score: isAccepted ? 1 : 0, scoreCategory: 'nextEdit', })); const scoring = Scoring.create(reconstructedRecording, nesEdits); return scoring; } function splitRecordingAtRequestTime(altAction: IAlternativeAction): { recordingPriorToRequest: LogEntry[]; recordingAfterRequest: LogEntry[]; } | undefined { if (!altAction.recording) { return undefined; } const recording = altAction.recording.entries; if (!recording || recording.length === 0) { return undefined; } const requestTime = altAction.recording.requestTime; const recordingIdxOfRequestTime = binarySearch(recording, (entry: LogEntry) => { if (entry.kind === 'meta') { return -1; } else { return entry.time - requestTime; } }); if (recordingIdxOfRequestTime === -1) { log('Request time is before any recording entries'); return undefined; } const recordingPriorToRequest = recording.slice(0, recordingIdxOfRequestTime + 1); const recordingAfterRequest = recording.slice(recordingIdxOfRequestTime + 1); return { recordingPriorToRequest, recordingAfterRequest }; } function documentIndexMapping(recording: LogEntry[]): Map { const map = new Map(); for (const entry of recording) { if (entry.kind === 'documentEncountered') { map.set(entry.id, entry.relativePath); } } return map; } function determineCurrentFileId(recording: LogEntry[]): number | undefined { let fileId: number | undefined; for (let i = recording.length - 1; i >= 0; i--) { const entry = recording[i]; if ('id' in entry) { fileId = entry.id; break; } } return fileId; } function getNextUserEdit(currentFile: { id: number; relativePath: string }, recordingBeforeRequest: LogEntry[], recordingAfterRequest: LogEntry[]): NextUserEdit.t { const N_EDITS_LIMIT = 10; const serializedEdits: ISerializedEdit[] = []; for (const entry of recordingAfterRequest) { if (entry.kind === 'changed' && 'id' in entry && entry.id === currentFile.id) { serializedEdits.push(entry.edit); } if (serializedEdits.length > N_EDITS_LIMIT) { break; } } const edits = new Edits( StringEdit, serializedEdits.map(se => new StringEdit(se.map(r => new StringReplacement(new OffsetRange(r[0], r[1]), r[2])))) ); return { edit: edits.compose().replacements.map(r => [r.replaceRange.start, r.replaceRange.endExclusive, r.newText] as const), relativePath: currentFile.relativePath, originalOpIdx: recordingBeforeRequest.length - 1 }; } } ================================================ FILE: script/alternativeAction/types.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Raw } from '@vscode/prompt-tsx'; import { IAlternativeAction, NextEditTelemetryStatus } from '../../src/extension/inlineEdits/node/nextEditProviderTelemetry'; import { LogEntry } from '../../src/platform/workspaceRecorder/common/workspaceLog'; import { ISerializedEdit } from '../logRecordingTypes'; export type IStringReplacement = [start: number, endEx: number, text: string]; export type IData = { prompt: Raw.ChatMessage[]; response: string; altAction: IAlternativeAction; postProcessingOutcome: { suggestedEdit: string; // example: "[978, 1021) -> \"foo\""; isInlineCompletion: boolean; }; suggestionStatus: NextEditTelemetryStatus; } export namespace NextUserEdit { export type t = { edit: ISerializedEdit; relativePath: string; originalOpIdx: number; }; } export namespace Recording { export type t = { log: LogEntry[]; nextUserEdit: { edit: ISerializedEdit; relativePath: string; originalOpIdx: number; }; } } export namespace SuggestedEdit { export type t = { documentUri: string; edit: ISerializedEdit; scoreCategory: 'nextEdit'; score: number; } } export namespace Scoring { export type t = { "$web-editor.format-json": true; "$web-editor.default-url": "https://microsoft.github.io/vscode-workbench-recorder-viewer/?editRating"; edits: SuggestedEdit.t[]; scoringContext: { kind: 'recording'; recording: Recording.t; }; }; export function create(recording: Recording.t, edits: SuggestedEdit.t[]): Scoring.t { return { "$web-editor.format-json": true, "$web-editor.default-url": "https://microsoft.github.io/vscode-workbench-recorder-viewer/?editRating", edits, scoringContext: { kind: 'recording', recording } }; } } ================================================ FILE: script/alternativeAction/util.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ const DEBUG = true; export function log(...args: any[]) { if (DEBUG) { console.log(...args); } } export function binarySearch( array: readonly T[], compare: (element: T) => number ): number { let left = 0; let right = array.length - 1; let lastLess = -1; while (left <= right) { const mid = Math.floor((left + right) / 2); const cmp = compare(array[mid]); if (cmp === 0) { return mid; } else if (cmp < 0) { lastLess = mid; left = mid + 1; } else { right = mid - 1; } } return lastLess; } //#region Either export type Either = Left | Right; export namespace Either { export function left(value: L): Left { return new Left(value); } export function right(value: R): Right { return new Right(value); } } /** * To instantiate a Left, use `Either.left(value)`. * To instantiate a Right, use `Either.right(value)`. */ class Left { constructor(readonly value: L) { } isLeft(): this is Left { return true; } isRight(): this is Right { return false; } } /** * To instantiate a Left, use `Either.left(value)`. * To instantiate a Right, use `Either.right(value)`. */ class Right { constructor(readonly value: R) { } isLeft(): this is Left { return false; } isRight(): this is Right { return true; } } //#endregion ================================================ FILE: script/analyzeEdits.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { promises as fs } from 'fs'; import * as path from 'path'; import * as readline from 'readline'; // Edit tool names we're tracking const EDIT_TOOL_NAMES = ['insert_edit_into_file', 'replace_string_in_file', 'multi_replace_string_in_file', 'apply_patch']; // Tool names that indicate a continuation/retry attempt const CONTINUATION_TOOL_NAMES = ['read_file']; interface ToolCall { tool: string; input_tokens?: number; cached_input_tokens?: number; output_tokens?: number; response: string | string[]; edits?: Array<{ path: string; edits: { replacements: Array<{ replaceRange: { start: number; endExclusive: number }; newText: string; }>; }; }>; } interface EditOperation { toolName: string; timestamp: string; success: boolean; filePath?: string; turnIndex: number; isRetry: boolean; retrySucceeded?: boolean; } interface ConversationAnalysis { conversationPath: string; edits: EditOperation[]; totalEdits: number; successfulEdits: number; failedEdits: number; successfulEditsWithRetries: number; totalUniqueEdits: number; modelName?: string; } interface RunAnalysis { runId: string; conversations: ConversationAnalysis[]; totalEdits: number; successRate: number; successRateWithRetries: number; totalUniqueEdits: number; modelName?: string; } async function listRuns(amlOutPath: string): Promise { const entries = await fs.readdir(amlOutPath, { withFileTypes: true }); // Filter directories that are numeric run IDs const runs = entries .filter(e => e.isDirectory() && /^\d+$/.test(e.name)) .map(e => e.name) .sort((a, b) => parseInt(b) - parseInt(a)); // Sort descending (newest first) return runs; } async function promptUserForRun(runs: string[]): Promise { console.log('\nAvailable test runs (newest first):'); runs.slice(0, 10).forEach((run, i) => { console.log(` ${i + 1}. ${run}`); }); if (runs.length > 10) { console.log(` ... and ${runs.length - 10} more`); } const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => { rl.question('\nEnter run number (or press Enter for the most recent): ', (answer) => { rl.close(); const choice = answer.trim(); if (choice === '') { resolve(runs[0]); } else { const index = parseInt(choice) - 1; if (index >= 0 && index < runs.length) { resolve(runs[index]); } else { console.log('Invalid selection, using most recent run.'); resolve(runs[0]); } } }); }); } async function analyzeConversation(conversationPath: string): Promise { const trajectoryPath = path.join(conversationPath, 'trajectories', 'trajectory.json'); let toolCalls: ToolCall[] = []; let modelName: string | undefined; try { const content = await fs.readFile(trajectoryPath, 'utf-8'); toolCalls = JSON.parse(content); } catch (error) { console.warn(`Could not read trajectory file: ${trajectoryPath}`); return { conversationPath, edits: [], totalEdits: 0, successfulEdits: 0, failedEdits: 0, successfulEditsWithRetries: 0, totalUniqueEdits: 0 }; } const edits: EditOperation[] = []; let turnIndex = 0; for (let i = 0; i < toolCalls.length; i++) { const toolCall = toolCalls[i]; if (!EDIT_TOOL_NAMES.includes(toolCall.tool)) { continue; } // Determine success based on response const response = Array.isArray(toolCall.response) ? toolCall.response[0] : toolCall.response; const success = typeof response === 'string' && response.includes('successfully edited'); // Get file path from edits if available const filePath = toolCall.edits && toolCall.edits.length > 0 ? toolCall.edits[0].path : undefined; // Detect retry pattern: failed edit -> continuation tool -> another edit let isRetry = false; let retrySucceeded: boolean | undefined; if (!success) { // Look ahead to see if there's a continuation tool followed by another edit let j = i + 1; let foundContinuationTool = false; while (j < toolCalls.length && j < i + 10) { // Look ahead max 10 calls if (CONTINUATION_TOOL_NAMES.includes(toolCalls[j].tool)) { foundContinuationTool = true; } else if (foundContinuationTool && EDIT_TOOL_NAMES.includes(toolCalls[j].tool)) { // Found a retry! isRetry = true; const retryResponse = Array.isArray(toolCalls[j].response) ? toolCalls[j].response[0] : toolCalls[j].response; retrySucceeded = typeof retryResponse === 'string' && retryResponse.includes('successfully edited'); break; } else if (EDIT_TOOL_NAMES.includes(toolCalls[j].tool)) { // Another edit without continuation tool in between, not a retry break; } j++; } } edits.push({ toolName: toolCall.tool, timestamp: new Date().toISOString(), // Trajectory doesn't have timestamps, use current time success, filePath, turnIndex: turnIndex++, isRetry, retrySucceeded }); } const successfulEdits = edits.filter(e => e.success).length; // Calculate success rate accounting for retries (final outcome only) const editsWithRetries = edits.filter(e => !e.success && e.isRetry); const retriedSuccesses = editsWithRetries.filter(e => e.retrySucceeded).length; const successfulEditsWithRetries = successfulEdits + retriedSuccesses; const totalUniqueEdits = edits.length - editsWithRetries.length + editsWithRetries.filter(e => e.retrySucceeded !== undefined).length; return { conversationPath, edits, totalEdits: edits.length, successfulEdits, failedEdits: edits.length - successfulEdits, successfulEditsWithRetries, totalUniqueEdits, modelName }; } async function analyzeRun(runId: string, basePath: string): Promise { const runPath = path.join(basePath, runId); const conversations: ConversationAnalysis[] = []; try { const entries = await fs.readdir(runPath, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { const conversationPath = path.join(runPath, entry.name); const analysis = await analyzeConversation(conversationPath); if (analysis.totalEdits > 0) { conversations.push(analysis); } } } } catch (error) { console.error(`Error reading run directory: ${error}`); } const totalEdits = conversations.reduce((sum, c) => sum + c.totalEdits, 0); const totalSuccessful = conversations.reduce((sum, c) => sum + c.successfulEdits, 0); const totalSuccessfulWithRetries = conversations.reduce((sum, c) => sum + c.successfulEditsWithRetries, 0); const totalUniqueEdits = conversations.reduce((sum, c) => sum + c.totalUniqueEdits, 0); // Get model name from first conversation that has one const modelName = conversations.find(c => c.modelName)?.modelName; return { runId, conversations, totalEdits, successRate: totalEdits > 0 ? totalSuccessful / totalEdits : 0, successRateWithRetries: totalUniqueEdits > 0 ? totalSuccessfulWithRetries / totalUniqueEdits : 0, totalUniqueEdits, modelName }; } function generateHTML(analysis: RunAnalysis, outputPath: string, includeRetries: boolean = false): string { // Build Sankey data const sankeyNodes: string[] = []; const sankeyLinks: Array<{ source: number; target: number; value: number }> = []; const nodeMap = new Map(); const getNodeIndex = (name: string): number => { if (!nodeMap.has(name)) { nodeMap.set(name, sankeyNodes.length); sankeyNodes.push(name); } return nodeMap.get(name)!; }; // Track flows const flows = new Map(); for (const conv of analysis.conversations) { for (const edit of conv.edits) { const toolNode = edit.toolName; // Check if this is a failed edit with a retry if (includeRetries && !edit.success && edit.isRetry && edit.retrySucceeded !== undefined) { // Show full retry flow: Tool -> Failed -> read_file -> Retry Edit -> Final Result const failedNode = 'Failed (will retry)'; const readFileNode = 'read_file'; const retryEditNode = `${toolNode} (retry)`; const finalResult = edit.retrySucceeded ? 'Success' : 'Failed'; flows.set(`${toolNode}->${failedNode}`, (flows.get(`${toolNode}->${failedNode}`) || 0) + 1); flows.set(`${failedNode}->${readFileNode}`, (flows.get(`${failedNode}->${readFileNode}`) || 0) + 1); flows.set(`${readFileNode}->${retryEditNode}`, (flows.get(`${readFileNode}->${retryEditNode}`) || 0) + 1); flows.set(`${retryEditNode}->${finalResult}`, (flows.get(`${retryEditNode}->${finalResult}`) || 0) + 1); continue; } // Tool -> Success/Fail const resultNode = edit.success ? 'Success' : 'Failed'; const flowKey = `${toolNode}->${resultNode}`; flows.set(flowKey, (flows.get(flowKey) || 0) + 1); } } // Convert flows to Sankey links for (const [flowKey, count] of flows.entries()) { const [source, target] = flowKey.split('->'); sankeyLinks.push({ source: getNodeIndex(source), target: getNodeIndex(target), value: count }); } // Build table rows const tableRows = analysis.conversations.flatMap(conv => conv.edits.map(edit => ({ conversation: path.basename(conv.conversationPath), toolName: edit.toolName, timestamp: edit.timestamp, success: edit.success, turnIndex: edit.turnIndex, isRetry: edit.isRetry, retrySucceeded: edit.retrySucceeded, filePath: edit.filePath })) ); const html = ` Run ${analysis.runId}${analysis.modelName ? ' - ' + analysis.modelName : ''}

🔧 Run ${analysis.runId}${analysis.modelName ? ' - ' + analysis.modelName : ''}

Analysis of edit tool operations and success rates

Total Edits
${analysis.totalEdits}
Success Rate
${(analysis.successRate * 100).toFixed(1)}%
Conversations
${analysis.conversations.length}

Edit Operations

${tableRows.map(row => ` `).join('')}
Conversation Tool Turn File Status Retry
${row.conversation} ${row.toolName} ${row.turnIndex} ${row.filePath || '-'} ${row.success ? '✓ Success' : '✗ Failed'} ${row.isRetry ? (row.retrySucceeded === true ? '✓ Retry Success' : row.retrySucceeded === false ? '✗ Retry Failed' : 'Retry Pending') : '-'}
`; return html; } async function main() { const args = process.argv.slice(2); const runIdArg = args.find(arg => arg.startsWith('--runId=')); const basePath = path.join('/Users/connor/Github/vscode-copilot-evaluation/.msbenchRun'); let runId: string; if (runIdArg) { runId = runIdArg.split('=')[1]; console.log(`Using run ID: ${runId}`); } else { const runs = await listRuns(basePath); if (runs.length === 0) { console.error('No test runs found in', basePath); process.exit(1); } runId = await promptUserForRun(runs); console.log(`Selected run: ${runId}`); } console.log('\nAnalyzing run...'); const analysis = await analyzeRun(runId, basePath); console.log(`\nFound ${analysis.conversations.length} conversations with edits`); console.log(`Total edits: ${analysis.totalEdits}`); console.log(`Success rate: ${(analysis.successRate * 100).toFixed(1)}%`); const outputPath = path.join(basePath, runId, 'edit-analysis.html'); const html = generateHTML(analysis, outputPath); await fs.writeFile(outputPath, html, 'utf-8'); console.log(`\n✓ Analysis complete! Generated: ${outputPath}`); } main().catch(console.error); ================================================ FILE: script/applyLocalDts.sh ================================================ #--------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. #--------------------------------------------------------------------------------------------- echo "Push your proposal changes and run \`npm run vscode-dts:dev\` instead." ================================================ FILE: script/build/compressTikToken.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { mkdir, readFile, writeFile } from 'fs/promises'; import * as path from 'path'; import { parseTikTokenBinary } from '../../src/platform/tokenizer/node/parseTikTokens'; import { writeVariableLengthQuantity } from '../../src/util/common/variableLengthQuantity'; /** * Compresses a `.tiktoken` file into a much more compact binary format. * * A tiktoken file is a list of base64 encoded terms, followed by a space * and (rather unnecessarily) by their index, like * ``` * IQ== 0 * Ig== 1 * Iw== 2 * JA== 3 * JQ== 4 * Jg== 5 * Jw== 6 * KA== 7 * ``` * * This compression takes advantage of the fact that term lengths increase * monotonically with their index. Each term is represented by a VLQ-encoded * length followed by the term itself. * * I explored doing a fancier format with "runs" of certain lengths, however * the difference was only a byte or two in exchange for much higher complexity. */ export async function compressTikToken(inputFile: string, outputFile: string) { const raw = await readFile(inputFile, 'utf-8'); const terms: Buffer[] = []; for (const line of raw.split('\n')) { if (!line) { continue; } const [base64, iStr] = line.split(' '); const i = Number(iStr); if (isNaN(Number(i))) { throw new Error(`malformed line ${line}`); } if (i !== terms.length) { throw new Error('non-monotonic index'); } terms.push(Buffer.from(base64, 'base64')); } const output: Uint8Array[] = []; for (const term of terms) { output.push(writeVariableLengthQuantity(term.length).buffer); output.push(term); } await mkdir(path.dirname(outputFile), { recursive: true }); await writeFile(outputFile, Buffer.concat(output)); assertOk(outputFile, terms); } function assertOk(outputFile: string, terms: Buffer[]) { const parsed = parseTikTokenBinary(outputFile); const actual: string[] = []; for (const [term, index] of parsed) { actual[index] = Buffer.from(term).toString('base64'); } assert.deepStrictEqual(actual, terms.map(t => t.toString('base64'))); } ================================================ FILE: script/build/copyStaticAssets.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as path from 'path'; import * as fs from 'fs'; const REPO_ROOT = path.join(__dirname, '..', '..'); export async function copyStaticAssets(srcpaths: string[], dst: string): Promise { await Promise.all(srcpaths.map(async srcpath => { const src = path.join(REPO_ROOT, srcpath); const dest = path.join(REPO_ROOT, dst, path.basename(srcpath)); await fs.promises.mkdir(path.dirname(dest), { recursive: true }); await fs.promises.copyFile(src, dest); })); } ================================================ FILE: script/build/downloadBinary.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as crypto from 'crypto'; import * as fs from 'fs'; import * as https from 'https'; import * as path from 'path'; import * as tar from 'tar'; import * as zlib from 'zlib'; const REPO_ROOT = path.join(__dirname, '..', '..'); export interface IBinary { url: string; sha256: string; destination: string; } export async function ensureBinary(binary: IBinary) { const binaryPath = path.join(REPO_ROOT, binary.destination); if (fs.existsSync(binaryPath)) { const sha256 = await computeSha256(binaryPath); if (sha256 === binary.sha256) { console.log(`Binary ${binary.destination} already exists and matches expected checksum.`); return; } console.log(`Binary ${binary.destination} already exists but does not match expected checksum. \n - Expected: ${binary.sha256}\n - Actual: ${sha256}\nRe-downloading...`); } console.log(`Downloading binary ${binary.destination}...`); await fs.promises.mkdir(path.dirname(binaryPath), { recursive: true }); const tempPath = path.join(path.dirname(binaryPath), crypto.randomUUID() + '.tgz'); try { await downloadFile(binary.url, tempPath); await untar(tempPath, path.dirname(binaryPath), /*strip*/2); const sha256 = await computeSha256(binaryPath); if (sha256 !== binary.sha256) { throw new Error(`Downloaded binary ${binary.destination} does not match expected checksum. Expected: ${binary.sha256}, actual: ${sha256}.`); } } finally { await fs.promises.unlink(tempPath); } } export function computeSha256(filePath: string): Promise { return new Promise((resolve, reject) => { const hash = crypto.createHash('sha256'); const stream = fs.createReadStream(filePath); stream.on('error', reject); stream.on('data', (chunk) => hash.update(chunk)); stream.on('end', () => resolve(hash.digest('hex'))); }); } export function downloadFile(url: string, tempPath: string, headers?: Record): Promise { return new Promise((resolve, reject) => { https.get(url, { headers }, (response) => { if (response.headers.location) { console.log(`Following redirect to ${response.headers.location}`); return downloadFile(response.headers.location, tempPath).then(resolve, reject); } if (response.statusCode === 404) { return reject(new Error(`File not found: ${url}`)); } const file = fs.createWriteStream(tempPath); response.pipe(file); file.on('finish', () => { file.close(); resolve(); }); }).on('error', (err) => { fs.unlink(tempPath, () => reject(err)); }); }); } export function get(url: string, opts: https.RequestOptions): Promise { return new Promise((resolve, reject) => { let result = ''; https.get(url, opts, response => { if (response.headers.location) { console.log(`Following redirect to ${response.headers.location}`); get(response.headers.location, opts).then(resolve, reject); } if (response.statusCode !== 200) { reject(new Error('Request failed: ' + response.statusCode)); } response.on('data', d => { result += d.toString(); }); response.on('end', () => { resolve(result); }); response.on('error', e => { reject(e); }); }); }); } export function untar(filePath: string, destination: string, strip?: number): Promise { return new Promise((resolve, reject) => { const readStream = fs.createReadStream(filePath); const writeStream = zlib.createGunzip(); const extractStream = tar.extract({ cwd: destination, strip, strict: true, onentry: (entry: any) => { console.log(`Extracting ${entry.path}`); } }); readStream.on('error', reject); writeStream.on('error', reject); extractStream.on('error', reject); extractStream.on('end', () => { resolve(); }); readStream.pipe(writeStream).pipe(extractStream); }); } ================================================ FILE: script/build/extractChatLib.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { exec } from 'child_process'; import * as fs from 'fs'; import { glob } from 'glob'; import * as jsonc from 'jsonc-parser'; import * as path from 'path'; import { promisify } from 'util'; const REPO_ROOT = path.join(__dirname, '..', '..'); const CHAT_LIB_DIR = path.join(REPO_ROOT, 'chat-lib'); const TARGET_DIR = path.join(CHAT_LIB_DIR, 'src'); const execAsync = promisify(exec); // Entry point - follow imports from the main chat-lib file // Note: All *.ts files in src/lib/node/test/ are automatically included const entryPoints = [ 'src/lib/node/chatLibMain.ts', 'src/util/vs/base-common.d.ts', 'src/util/vs/vscode-globals-nls.d.ts', 'src/util/vs/vscode-globals-product.d.ts', 'src/util/common/globals.d.ts', 'src/util/common/test/shims/vscodeTypesShim.ts', 'src/platform/diff/common/diffWorker.ts', 'src/platform/tokenizer/node/tikTokenizerWorker.ts', // For tests: 'src/platform/authentication/test/node/simulationTestCopilotTokenManager.ts', 'src/extension/completions-core/vscode-node/lib/src/test/textDocument.ts', ]; interface FileInfo { srcPath: string; destPath: string; relativePath: string; dependencies: string[]; } class ChatLibExtractor { private processedFiles = new Set(); private allFiles = new Map(); private pathMappings: Map = new Map(); async extract(): Promise { // Load path mappings from tsconfig.json await this.loadPathMappings(); console.log('Starting chat-lib extraction...'); // Clean target directory await this.cleanTargetDir(); // Process entry points and their dependencies await this.processEntryPoints(); // Copy all processed files await this.copyFiles(); // Use static module files await this.generateModuleFiles(); // Validate the module await this.validateModule(); // Compile TypeScript to validate await this.compileTypeScript(); console.log('Chat-lib extraction completed successfully!'); } private async loadPathMappings(): Promise { const tsconfigPath = path.join(REPO_ROOT, 'tsconfig.json'); const tsconfigContent = await fs.promises.readFile(tsconfigPath, 'utf-8'); const tsconfig = jsonc.parse(tsconfigContent); if (tsconfig.compilerOptions?.paths) { for (const [alias, targets] of Object.entries(tsconfig.compilerOptions.paths)) { // Skip the 'vscode' mapping as it's handled separately if (alias === 'vscode') { continue; } // Handle path mappings like "#lib/*" -> ["./src/extension/completions-core/lib/src/*"] // and "#types" -> ["./src/extension/completions-core/types/src"] if (Array.isArray(targets) && targets.length > 0) { const target = targets[0]; // Use the first target // Remove leading './' and trailing '/*' if present const cleanTarget = target.replace(/^\.\//, '').replace(/\/\*$/, ''); const cleanAlias = alias.replace(/\/\*$/, ''); this.pathMappings.set(cleanAlias, cleanTarget); } } } console.log('Loaded path mappings:', Array.from(this.pathMappings.entries())); } private async cleanTargetDir(): Promise { // Remove and recreate the src directory if (fs.existsSync(TARGET_DIR)) { await fs.promises.rm(TARGET_DIR, { recursive: true, force: true }); } await fs.promises.mkdir(TARGET_DIR, { recursive: true }); } private async processEntryPoints(): Promise { console.log('Processing entry points and dependencies...'); // Start with static entry points and dynamically add all test files const testFiles = await glob('src/lib/vscode-node/test/*.ts', { cwd: REPO_ROOT }); const queue = [...entryPoints, ...testFiles]; while (queue.length > 0) { const filePath = queue.shift()!; if (this.processedFiles.has(filePath)) { continue; } const fullPath = path.join(REPO_ROOT, filePath); if (!fs.existsSync(fullPath)) { console.warn(`Warning: File not found: ${filePath}`); continue; } const dependencies = await this.extractDependencies(fullPath); const destPath = this.getDestinationPath(filePath); this.allFiles.set(filePath, { srcPath: fullPath, destPath, relativePath: filePath, dependencies }); this.processedFiles.add(filePath); // Add dependencies to queue dependencies.forEach(dep => { if (!this.processedFiles.has(dep)) { queue.push(dep); } }); } } private async extractDependencies(filePath: string): Promise { const content = await fs.promises.readFile(filePath, 'utf-8'); const dependencies: string[] = []; // Remove single-line comments and process line by line to avoid matching commented imports // We need to be careful not to remove strings that contain '//' const lines = content.split('\n'); const activeLines: string[] = []; let inBlockComment = false; for (const line of lines) { // Track block comments if (line.trim().startsWith('/*')) { // preserve pragmas in tsx files if (!(filePath.endsWith('.tsx') && line.match(/\/\*\*\s+@jsxImportSource\s+\S+/))) { inBlockComment = true; } } if (inBlockComment) { if (line.includes('*/')) { inBlockComment = false; } continue; } // Skip single-line comments const trimmedLine = line.trim(); if (trimmedLine.startsWith('//')) { continue; } // For lines that might have inline comments, we need to preserve string content // Remove comments that are not inside strings let processedLine = line; // Simple heuristic: if the line contains import/export, keep everything up to // // that's outside of string literals if (trimmedLine.includes('import') || trimmedLine.includes('export')) { // Remove inline comments (this is a simple approach - could be improved) const commentIndex = line.indexOf('//'); if (commentIndex !== -1) { // Check if // is inside a string by counting quotes before it const beforeComment = line.substring(0, commentIndex); const singleQuotes = (beforeComment.match(/'/g) || []).length; const doubleQuotes = (beforeComment.match(/"/g) || []).length; // If even number of quotes, the comment is outside strings if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0) { processedLine = beforeComment; } } } activeLines.push(processedLine); } const activeContent = activeLines.join('\n'); // Extract both import and export statements using regex // Matches: // - import ... from './path' // - export ... from './path' // - export { ... } from './path' // Updated regex to match all relative imports (including multiple ../ segments) const relativeImportRegex = /(?:import(?:\s+type)?|export)\s+(?:(?:\{[^}]*\}|\*(?:\s+as\s+\w+)?|\w+)\s+from\s+)?['"](\.\.?\/[^'"]*)['"]/g; let match; while ((match = relativeImportRegex.exec(activeContent)) !== null) { const importPath = match[1]; const resolvedPath = this.resolveImportPath(filePath, importPath); if (resolvedPath) { dependencies.push(resolvedPath); } } // Also match path alias imports like: import ... from '#lib/...' or '#types' // We need to resolve these to follow their dependencies const aliasImportRegex = /(?:import(?:\s+type)?|export)\s+(?:(?:\{[^}]*\}|\*(?:\s+as\s+\w+)?|\w+)\s+from\s+)?['"]([#][^'"]*)['"]/g; while ((match = aliasImportRegex.exec(activeContent)) !== null) { const importPath = match[1]; const resolvedPath = this.resolvePathAlias(importPath); if (resolvedPath) { dependencies.push(resolvedPath); } } // For tsx files process JSX imports as well if (filePath.endsWith('.tsx')) { const jsxRelativeImportRegex = /\/\*\*\s+@jsxImportSource\s+(\.\.?\/\S+)\s+\*\//g; while ((match = jsxRelativeImportRegex.exec(activeContent)) !== null) { const importPath = match[1]; const resolvedPath = this.resolveImportPath(filePath, path.join(importPath, 'jsx-runtime')); if (resolvedPath) { dependencies.push(resolvedPath); } } } return dependencies; } private resolvePathAlias(importPath: string): string | null { // Handle path alias imports like '#lib/foo' or '#types' // Find the matching alias by checking if the import starts with any registered alias for (const [alias, targetPath] of this.pathMappings.entries()) { if (importPath === alias) { // Exact match for aliases without wildcards (e.g., '#types') return this.resolveFileWithExtensions(path.join(REPO_ROOT, targetPath)); } else if (importPath.startsWith(alias + '/')) { // Wildcard match for aliases with /* (e.g., '#lib/foo' matches '#lib') const remainder = importPath.substring(alias.length + 1); // +1 to skip the '/' const fullPath = path.join(REPO_ROOT, targetPath, remainder); return this.resolveFileWithExtensions(fullPath); } } // If no alias matched, return null console.warn(`Warning: Path alias not found for: ${importPath}`); return null; } private resolveFileWithExtensions(basePath: string): string | null { // Try with .ts extension if (fs.existsSync(basePath + '.ts')) { return this.normalizePath(path.relative(REPO_ROOT, basePath + '.ts')); } // Try with .tsx extension if (fs.existsSync(basePath + '.tsx')) { return this.normalizePath(path.relative(REPO_ROOT, basePath + '.tsx')); } // Try with .d.ts extension if (fs.existsSync(basePath + '.d.ts')) { return this.normalizePath(path.relative(REPO_ROOT, basePath + '.d.ts')); } // Try with index.ts if (fs.existsSync(path.join(basePath, 'index.ts'))) { return this.normalizePath(path.relative(REPO_ROOT, path.join(basePath, 'index.ts'))); } // Try with index.tsx if (fs.existsSync(path.join(basePath, 'index.tsx'))) { return this.normalizePath(path.relative(REPO_ROOT, path.join(basePath, 'index.tsx'))); } // Try with index.d.ts if (fs.existsSync(path.join(basePath, 'index.d.ts'))) { return this.normalizePath(path.relative(REPO_ROOT, path.join(basePath, 'index.d.ts'))); } // Try as-is if (fs.existsSync(basePath)) { return this.normalizePath(path.relative(REPO_ROOT, basePath)); } return null; } private resolveImportPath(fromFile: string, importPath: string): string | null { const fromDir = path.dirname(fromFile); const resolved = path.resolve(fromDir, importPath); // If import path ends with .js, try replacing with .ts/.tsx first if (importPath.endsWith('.js')) { const baseResolved = resolved.slice(0, -3); // Remove .js if (fs.existsSync(baseResolved + '.ts')) { return this.normalizePath(path.relative(REPO_ROOT, baseResolved + '.ts')); } if (fs.existsSync(baseResolved + '.tsx')) { return this.normalizePath(path.relative(REPO_ROOT, baseResolved + '.tsx')); } } // Try with .ts extension if (fs.existsSync(resolved + '.ts')) { return this.normalizePath(path.relative(REPO_ROOT, resolved + '.ts')); } // Try with .tsx extension if (fs.existsSync(resolved + '.tsx')) { return this.normalizePath(path.relative(REPO_ROOT, resolved + '.tsx')); } // Try with .d.ts extension if (fs.existsSync(resolved + '.d.ts')) { return this.normalizePath(path.relative(REPO_ROOT, resolved + '.d.ts')); } // Try with index.ts if (fs.existsSync(path.join(resolved, 'index.ts'))) { return this.normalizePath(path.relative(REPO_ROOT, path.join(resolved, 'index.ts'))); } // Try with index.tsx if (fs.existsSync(path.join(resolved, 'index.tsx'))) { return this.normalizePath(path.relative(REPO_ROOT, path.join(resolved, 'index.tsx'))); } // Try with index.d.ts if (fs.existsSync(path.join(resolved, 'index.d.ts'))) { return this.normalizePath(path.relative(REPO_ROOT, path.join(resolved, 'index.d.ts'))); } // Try as-is if (fs.existsSync(resolved)) { return this.normalizePath(path.relative(REPO_ROOT, resolved)); } // If we get here, the file was not found - throw an error throw new Error(`Import file not found: ${importPath} (resolved to ${resolved}) imported from ${fromFile}`); } private normalizePath(filePath: string): string { // Normalize path separators to forward slashes for consistency across platforms return filePath.replace(/\\/g, '/'); } private getDestinationPath(filePath: string): string { // Normalize the input path first, then convert src/... to _internal/... const normalizedPath = this.normalizePath(filePath); const relativePath = normalizedPath.replace(/^src\//, '_internal/'); return path.join(TARGET_DIR, relativePath); } private async copyFiles(): Promise { console.log(`Copying ${this.allFiles.size} files...`); for (const [, fileInfo] of this.allFiles) { // Skip the main entry point file since it becomes top-level main.ts if (fileInfo.relativePath === 'src/lib/node/chatLibMain.ts') { continue; } await fs.promises.mkdir(path.dirname(fileInfo.destPath), { recursive: true }); // Read source file const content = await fs.promises.readFile(fileInfo.srcPath, 'utf-8'); // Transform content to replace vscode imports and fix relative paths const transformedContent = this.transformFileContent(content, fileInfo.relativePath); // Write to destination await fs.promises.writeFile(fileInfo.destPath, transformedContent); } } private transformFileContent(content: string, filePath: string): string { let transformed = content; // Normalize path for consistent comparison across platforms const normalizedFilePath = this.normalizePath(filePath); // Rewrite non-type imports of 'vscode' to use vscodeTypesShim transformed = this.rewriteVscodeImports(transformed, normalizedFilePath); // Rewrite imports from local vscodeTypes to use vscodeTypesShim transformed = this.rewriteVscodeTypesImports(transformed, normalizedFilePath); // Rewrite imports in test files: '../../node/chatLibMain' -> '../../../../main' if (normalizedFilePath.startsWith('src/lib/vscode-node/test/')) { transformed = transformed.replace( /(from\s+['"])\.\.\/\.\.\/node\/chatLibMain(['"])/g, '$1../../../../main$2' ); } // Only rewrite relative imports for main.ts (chatLibMain.ts) if (normalizedFilePath === 'src/lib/node/chatLibMain.ts') { transformed = transformed.replace( /import\s+([^'"]*)\s+from\s+['"](\.\/[^'"]*|\.\.\/[^'"]*)['"]/g, (match, importClause, importPath) => { const rewrittenPath = this.rewriteImportPath(filePath, importPath); return `import ${importClause} from '${rewrittenPath}'`; } ); } return transformed; } private rewriteVscodeImports(content: string, filePath: string): string { // Don't rewrite vscode imports in the main vscodeTypes.ts file if (filePath === 'src/vscodeTypes.ts') { return content; } // Pattern to match import statements from 'vscode' // This regex captures: // - import * as vscode from 'vscode' // - import { Uri, window } from 'vscode' // - import vscode from 'vscode' // But NOT type-only imports like: // - import type { Uri } from 'vscode' // - import type * as vscode from 'vscode' const vscodeImportRegex = /^(\s*import\s+)(?!type\s+)([^'"]*)\s+from\s+['"]vscode['"];?\s*$/gm; return content.replace(vscodeImportRegex, (match, importPrefix, importClause) => { // Calculate the relative path to vscodeTypesShim based on the current file location const shimPath = this.getVscodeTypesShimPath(filePath); return `${importPrefix}${importClause.trim()} from '${shimPath}';`; }); } private rewriteVscodeTypesImports(content: string, filePath: string): string { // Don't rewrite vscodeTypes imports in the main vscodeTypes.ts file itself if (filePath === 'src/vscodeTypes.ts') { return content; } // Don't rewrite in the vscodeTypesShim file itself to avoid circular imports if (filePath === 'src/util/common/test/shims/vscodeTypesShim.ts') { return content; } // Pattern to match non-type imports from local vscodeTypes // This regex captures imports like: // - import { ChatErrorLevel } from '../../../vscodeTypes' // - import * as vscodeTypes from '../../../vscodeTypes' // But NOT type-only imports like: // - import type { ChatErrorLevel } from '../../../vscodeTypes' const vscodeTypesImportRegex = /^(\s*import\s+)(?!type\s+)([^'"]*)\s+from\s+['"]([^'"]*\/vscodeTypes)['"];?\s*$/gm; return content.replace(vscodeTypesImportRegex, (match, importPrefix, importClause, importPath) => { // Calculate the relative path to vscodeTypesShim based on the current file location const shimPath = this.getVscodeTypesShimPath(filePath); return `${importPrefix}${importClause.trim()} from '${shimPath}';`; }); } private getVscodeTypesShimPath(filePath: string): string { // For main.ts (chatLibMain.ts), use the _internal structure if (filePath === 'src/lib/node/chatLibMain.ts') { return './_internal/util/common/test/shims/vscodeTypesShim'; } // For other files, calculate relative path from their location to the shim // The target shim location will be: _internal/util/common/test/shims/vscodeTypesShim // Files are placed in: _internal/ // Remove 'src/' prefix and calculate depth const relativePath = filePath.replace(/^src\//, ''); const pathSegments = relativePath.split('/'); const depth = pathSegments.length - 1; // -1 because the last segment is the filename // Go up 'depth' levels, then down to the shim const upLevels = '../'.repeat(depth); return `${upLevels}util/common/test/shims/vscodeTypesShim`; } private rewriteImportPath(fromFile: string, importPath: string): string { // For main.ts, rewrite relative imports to use ./_internal structure if (fromFile === 'src/lib/node/chatLibMain.ts') { // Convert ../../extension/... to ./_internal/extension/... // Convert ../../platform/... to ./_internal/platform/... // Convert ../../util/... to ./_internal/util/... return importPath.replace(/^\.\.\/\.\.\//, './_internal/'); } // For other files, don't change the import path return importPath; } private async generateModuleFiles(): Promise { console.log('Using static module files already present in chat-lib directory...'); // Copy main.ts from src/lib/node/chatLibMain.ts const mainTsPath = path.join(REPO_ROOT, 'src', 'lib', 'node', 'chatLibMain.ts'); const mainTsContent = await fs.promises.readFile(mainTsPath, 'utf-8'); const transformedMainTs = this.transformFileContent(mainTsContent, 'src/lib/node/chatLibMain.ts'); await fs.promises.writeFile(path.join(TARGET_DIR, 'main.ts'), transformedMainTs); // Copy root package.json to chat-lib/src await this.copyRootPackageJson(); // Copy all vscode.proposed.*.d.ts files await this.copyVSCodeProposedTypes(); // Copy all tiktoken files await this.copyTikTokenFiles(); // Copy test reply files await this.copyTestReplyFiles(); // Update chat-lib tsconfig.json with path mappings await this.updateChatLibTsConfig(); } private async copyTestReplyFiles(): Promise { console.log('Copying test reply files...'); // Find all .reply.txt files in src/lib/vscode-node/test/ const testDir = path.join(REPO_ROOT, 'src', 'lib', 'vscode-node', 'test'); const replyFiles = await glob('*.reply.txt', { cwd: testDir }); for (const file of replyFiles) { const srcPath = path.join(testDir, file); const destPath = path.join(TARGET_DIR, '_internal', 'lib', 'vscode-node', 'test', file); await fs.promises.mkdir(path.dirname(destPath), { recursive: true }); await fs.promises.copyFile(srcPath, destPath); } console.log(`Copied ${replyFiles.length} test reply files`); } private async updateChatLibTsConfig(): Promise { console.log('Updating chat-lib tsconfig.json with path mappings...'); const chatLibTsconfigPath = path.join(CHAT_LIB_DIR, 'tsconfig.json'); const tsconfigContent = await fs.promises.readFile(chatLibTsconfigPath, 'utf-8'); const tsconfig = jsonc.parse(tsconfigContent); // Ensure compilerOptions exists if (!tsconfig.compilerOptions) { tsconfig.compilerOptions = {}; } // Ensure paths exists if (!tsconfig.compilerOptions.paths) { tsconfig.compilerOptions.paths = {}; } // Read the root tsconfig once to check for wildcards const rootTsconfigPath = path.join(REPO_ROOT, 'tsconfig.json'); const rootTsconfigContent = await fs.promises.readFile(rootTsconfigPath, 'utf-8'); const rootTsconfig = jsonc.parse(rootTsconfigContent); // Add path mappings from the root tsconfig, adjusted for chat-lib structure // The files are in src/_internal/... structure for (const [alias, targetPath] of this.pathMappings.entries()) { // Convert from root paths like "src/extension/completions-core/lib/src" // to chat-lib paths like "./src/_internal/extension/completions-core/lib/src" // Remove the "src/" prefix from targetPath since it's already part of the _internal structure const pathWithoutSrc = targetPath.replace(/^src\//, ''); const chatLibPath = `./src/_internal/${pathWithoutSrc}`; let aliasWithWildcard = alias; let pathWithWildcard = chatLibPath; // Check if the original mapping had a wildcard if (rootTsconfig.compilerOptions?.paths) { for (const key of Object.keys(rootTsconfig.compilerOptions.paths)) { const keyWithoutWildcard = key.replace(/\/\*$/, ''); if (keyWithoutWildcard === alias && key.endsWith('/*')) { aliasWithWildcard = alias + '/*'; pathWithWildcard = chatLibPath + '/*'; break; } } } tsconfig.compilerOptions.paths[aliasWithWildcard] = [pathWithWildcard]; } // Write the updated tsconfig back await fs.promises.writeFile( chatLibTsconfigPath, JSON.stringify(tsconfig, null, '\t') + '\n' ); console.log('Chat-lib tsconfig.json updated with path mappings:', Object.keys(tsconfig.compilerOptions.paths)); } private async validateModule(): Promise { console.log('Validating module...'); // Check if static files exist in chat-lib directory const staticFiles = ['package.json', 'tsconfig.json', 'README.md', 'LICENSE.txt']; for (const file of staticFiles) { const filePath = path.join(CHAT_LIB_DIR, file); if (!fs.existsSync(filePath)) { throw new Error(`Required static file missing: ${file}`); } } // Check if main.ts exists in src directory const mainTsPath = path.join(TARGET_DIR, 'main.ts'); if (!fs.existsSync(mainTsPath)) { throw new Error(`Required file missing: src/main.ts`); } console.log('Module validation passed!'); } private async copyVSCodeProposedTypes(): Promise { console.log('Copying VS Code proposed API types...'); // Find all vscode.proposed.*.d.ts files in src/extension/ const extensionDir = path.join(REPO_ROOT, 'src', 'extension'); const proposedTypeFiles = await glob('vscode.proposed.*.d.ts', { cwd: extensionDir }); for (const file of proposedTypeFiles) { const srcPath = path.join(extensionDir, file); const destPath = path.join(TARGET_DIR, '_internal', 'extension', file); await fs.promises.mkdir(path.dirname(destPath), { recursive: true }); await fs.promises.copyFile(srcPath, destPath); } console.log(`Copied ${proposedTypeFiles.length} VS Code proposed API type files`); } private async copyTikTokenFiles(): Promise { console.log('Copying tiktoken files...'); // Find all .tiktoken files in src/platform/tokenizer/node/ const tokenizerDir = path.join(REPO_ROOT, 'src', 'platform', 'tokenizer', 'node'); const tikTokenFiles = await glob('*.tiktoken', { cwd: tokenizerDir }); for (const file of tikTokenFiles) { const srcPath = path.join(tokenizerDir, file); const destPath = path.join(TARGET_DIR, '_internal', 'platform', 'tokenizer', 'node', file); await fs.promises.mkdir(path.dirname(destPath), { recursive: true }); await fs.promises.copyFile(srcPath, destPath); } console.log(`Copied ${tikTokenFiles.length} tiktoken files`); } private async copyRootPackageJson(): Promise { console.log('Copying root package.json to chat-lib/src...'); const srcPath = path.join(REPO_ROOT, 'package.json'); const destPath = path.join(TARGET_DIR, 'package.json'); await fs.promises.copyFile(srcPath, destPath); console.log('Root package.json copied successfully!'); // Update chat-lib package.json dependencies await this.updateChatLibDependencies(); } private async updateChatLibDependencies(): Promise { console.log('Updating chat-lib package.json dependencies...'); const rootPackageJsonPath = path.join(REPO_ROOT, 'package.json'); const chatLibPackageJsonPath = path.join(CHAT_LIB_DIR, 'package.json'); const rootPackageLockPath = path.join(REPO_ROOT, 'package-lock.json'); const chatLibPackageLockPath = path.join(CHAT_LIB_DIR, 'package-lock.json'); // Read both package.json files const rootPackageJson = JSON.parse(await fs.promises.readFile(rootPackageJsonPath, 'utf-8')); const chatLibPackageJson = JSON.parse(await fs.promises.readFile(chatLibPackageJsonPath, 'utf-8')); // Combine all dependencies and devDependencies from root const rootDependencies = { ...(rootPackageJson.dependencies || {}), ...(rootPackageJson.devDependencies || {}) }; let updatedCount = 0; let removedCount = 0; const changes: string[] = []; const updatedPackages = new Set(); // Update existing dependencies in chat-lib with versions from root for (const depType of ['dependencies', 'devDependencies']) { if (chatLibPackageJson[depType]) { const dependencyNames = Object.keys(chatLibPackageJson[depType]); for (const depName of dependencyNames) { if (rootDependencies[depName]) { // Update version if it exists in root const oldVersion = chatLibPackageJson[depType][depName]; const newVersion = rootDependencies[depName]; if (oldVersion !== newVersion) { chatLibPackageJson[depType][depName] = newVersion; changes.push(` Updated ${depName}: ${oldVersion} → ${newVersion}`); updatedCount++; updatedPackages.add(depName); } } else { // Remove dependency if it no longer exists in root delete chatLibPackageJson[depType][depName]; changes.push(` Removed ${depName} (no longer in root package.json)`); removedCount++; } } // Clean up empty dependency objects if (Object.keys(chatLibPackageJson[depType]).length === 0) { delete chatLibPackageJson[depType]; } } } // Write the updated chat-lib package.json await fs.promises.writeFile( chatLibPackageJsonPath, JSON.stringify(chatLibPackageJson, null, '\t') + '\n' ); console.log(`Chat-lib dependencies updated: ${updatedCount} updated, ${removedCount} removed`); if (changes.length > 0) { console.log('Changes made:'); changes.forEach(change => console.log(change)); } // Update package-lock.json for changed dependencies and their transitive dependencies if (updatedPackages.size > 0 && fs.existsSync(rootPackageLockPath) && fs.existsSync(chatLibPackageLockPath)) { console.log('Updating chat-lib package-lock.json for changed dependencies...'); const rootPackageLock = JSON.parse(await fs.promises.readFile(rootPackageLockPath, 'utf-8')); const chatLibPackageLock = JSON.parse(await fs.promises.readFile(chatLibPackageLockPath, 'utf-8')); // Update the root package entry with new dependencies if (chatLibPackageLock.packages && chatLibPackageLock.packages['']) { chatLibPackageLock.packages[''].dependencies = chatLibPackageJson.dependencies || {}; chatLibPackageLock.packages[''].devDependencies = chatLibPackageJson.devDependencies || {}; } // Collect all packages to update (direct dependencies + their transitive dependencies) const packagesToUpdate = new Set(); const queue: string[] = []; // Start with updated packages for (const pkgName of updatedPackages) { const pkgPath = `node_modules/${pkgName}`; queue.push(pkgPath); packagesToUpdate.add(pkgPath); } // Traverse dependency tree from root package-lock to find all transitive dependencies while (queue.length > 0) { const pkgPath = queue.shift()!; const pkgInfo = rootPackageLock.packages?.[pkgPath]; if (pkgInfo) { // Collect all dependency types const deps = { ...pkgInfo.dependencies, ...pkgInfo.optionalDependencies, ...pkgInfo.devDependencies }; for (const depName of Object.keys(deps)) { // Handle nested dependencies const nestedDepPath = `${pkgPath}/node_modules/${depName}`; const topLevelDepPath = `node_modules/${depName}`; let actualDepPath: string | null = null; if (rootPackageLock.packages[nestedDepPath]) { actualDepPath = nestedDepPath; } else if (rootPackageLock.packages[topLevelDepPath]) { actualDepPath = topLevelDepPath; } else { // Walk up the parent chain const pathParts = pkgPath.split('/node_modules/'); for (let i = pathParts.length - 1; i >= 0; i--) { const parentPath = pathParts.slice(0, i).join('/node_modules/'); const candidatePath = parentPath ? `${parentPath}/node_modules/${depName}` : `node_modules/${depName}`; if (rootPackageLock.packages[candidatePath]) { actualDepPath = candidatePath; break; } } } if (actualDepPath && !packagesToUpdate.has(actualDepPath)) { packagesToUpdate.add(actualDepPath); queue.push(actualDepPath); } } } } // Update package entries in chat-lib lock file let lockUpdatedCount = 0; for (const pkgPath of packagesToUpdate) { if (rootPackageLock.packages[pkgPath] && chatLibPackageLock.packages[pkgPath]) { chatLibPackageLock.packages[pkgPath] = rootPackageLock.packages[pkgPath]; lockUpdatedCount++; } } // Write the updated chat-lib package-lock.json await fs.promises.writeFile( chatLibPackageLockPath, JSON.stringify(chatLibPackageLock, null, '\t') + '\n' ); console.log(`Chat-lib package-lock.json updated: ${lockUpdatedCount} package entries updated`); } } private async compileTypeScript(): Promise { console.log('Compiling TypeScript to validate module...'); try { // Change to the chat-lib directory and run TypeScript compiler const { stdout, stderr } = await execAsync('npx tsc --noEmit', { cwd: CHAT_LIB_DIR, timeout: 60000 // 60 second timeout }); if (stderr) { console.warn('TypeScript compilation warnings:', stderr); } console.log('TypeScript compilation successful!'); } catch (error: any) { console.error('TypeScript compilation failed:', error.stdout || error.message); throw new Error(`TypeScript compilation failed: ${error.stdout || error.message}`); } } } // Main execution async function main(): Promise { try { const extractor = new ChatLibExtractor(); await extractor.extract(); } catch (error) { console.error('Extraction failed:', error); process.exit(1); } } if (require.main === module) { main(); } ================================================ FILE: script/build/moveProposedDts.js ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ const fs = require('fs'); const path = require('path'); const files = fs.readdirSync('.').filter(f => f.startsWith('vscode.') && f.endsWith('.ts')); for (const f of files) { fs.renameSync(f, path.join('src', 'extension', f)); } ================================================ FILE: script/build/vscodeDtsCheck.js ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ // Usage: node script/build/vscodeDtsCheck.js // Reads vscodeCommit from package.json, re-downloads proposed d.ts files // at that commit, checks if any differ from what's committed, then restores // the originals. Exits with code 1 if files are out of date. const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const targetDir = path.resolve('src', 'extension'); function main() { const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8')); const sha = pkg.vscodeCommit; if (!sha) { console.error('No vscodeCommit found in package.json. Run "npm run vscode-dts:update" first.'); process.exit(1); } console.log(`Checking proposed d.ts files against vscodeCommit: ${sha}`); // Download proposed d.ts files using the commit SHA execSync(`node node_modules/@vscode/dts/index.js dev ${sha}`, { stdio: 'inherit' }); // Compare downloaded files with committed ones const downloaded = fs.readdirSync('.').filter(f => f.startsWith('vscode.') && f.endsWith('.ts')); const mismatched = []; for (const f of downloaded) { const committedPath = path.join(targetDir, f); const newContent = fs.readFileSync(f, 'utf-8'); if (!fs.existsSync(committedPath)) { mismatched.push(f + ' (missing)'); } else { const oldContent = fs.readFileSync(committedPath, 'utf-8'); if (oldContent !== newContent) { mismatched.push(f); } } // Clean up the downloaded file fs.unlinkSync(f); } if (mismatched.length > 0) { console.error('The following proposed API type definitions are out of date:'); for (const f of mismatched) { console.error(` - ${f}`); } console.error('Run "npm run vscode-dts:update" and commit the changes.'); process.exit(1); } console.log('All proposed API type definitions are up to date.'); } main(); ================================================ FILE: script/build/vscodeDtsUpdate.js ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ // Usage: node script/build/vscodeDtsUpdate.js [branch] // Downloads proposed API d.ts files from the given branch (default: main) // of microsoft/vscode and writes the resolved commit SHA to package.json. const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const https = require('https'); const branch = process.argv[2] || 'main'; function resolveCommitSha(branch) { return new Promise((resolve, reject) => { const options = { hostname: 'api.github.com', path: `/repos/microsoft/vscode/commits/${encodeURIComponent(branch)}`, headers: { 'User-Agent': 'vscode-copilot-chat', 'Accept': 'application/vnd.github.sha' } }; https.get(options, res => { if (res.statusCode !== 200) { reject(new Error(`Failed to resolve commit for branch "${branch}": HTTP ${res.statusCode}`)); return; } let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => resolve(data.trim())); }).on('error', reject); }); } async function main() { const sha = await resolveCommitSha(branch); console.log(`Resolved branch "${branch}" to commit ${sha}`); // Download proposed d.ts files using the commit SHA execSync(`node node_modules/@vscode/dts/index.js dev ${sha}`, { stdio: 'inherit' }); // Move downloaded files to src/extension/ const files = fs.readdirSync('.').filter(f => f.startsWith('vscode.') && f.endsWith('.ts')); for (const f of files) { fs.renameSync(f, path.join('src', 'extension', f)); } // Write the commit SHA to package.json const pkgPath = path.resolve('package.json'); const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); pkg.vscodeCommit = sha; fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, '\t') + '\n'); console.log(`Wrote vscodeCommit: ${sha} to package.json`); } main().catch(err => { console.error(err); process.exit(1); }); ================================================ FILE: script/cleanLog.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; import * as path from 'path'; function showHelp(): void { console.log(` cleanLog.ts - A utility script to filter log files by topic. This script reads a log file, filters entries to only include those matching a specified topic, strips timestamps from the output, and writes the filtered content back to the file. Usage: npx ts-node script/cleanLog.ts --log= Options: --log= The topic to filter by (case-insensitive). Required. --help Show this help message. Example: npx ts-node script/cleanLog.ts --log=InlineChat /path/to/extension.log This will filter the log file to only include entries containing [InlineChat] and overwrite the original file with the filtered content. `); } function parseArgs(args: string[]): { logTopic: string; filePath: string } | 'help' { if (args.includes('--help') || args.includes('-h')) { return 'help'; } let logTopic: string | undefined; let filePath: string | undefined; for (const arg of args) { if (arg.startsWith('--log=')) { logTopic = arg.slice('--log='.length); } else if (!arg.startsWith('-')) { filePath = arg; } } if (!logTopic) { throw new Error('Missing required argument: --log='); } if (!filePath) { throw new Error('Missing required positional argument: '); } return { logTopic, filePath }; } // Matches the start of a log line: timestamp [level] [TOPIC]... const LOG_LINE_PATTERN = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} \[/; // Matches the timestamp prefix to strip it const TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} /; function stripTimestamp(line: string): string { return line.replace(TIMESTAMP_PATTERN, ''); } function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function filterLogByTopic(content: string, topic: string): string { const lines = content.split('\n'); const result: string[] = []; const topicPattern = new RegExp(`\\[${escapeRegExp(topic)}\\]`, 'i'); let currentLogEntry: string[] = []; let keepCurrentEntry = false; function flushEntry() { if (keepCurrentEntry && currentLogEntry.length > 0) { // Strip timestamp from the first line of the entry currentLogEntry[0] = stripTimestamp(currentLogEntry[0]); result.push(...currentLogEntry); } currentLogEntry = []; keepCurrentEntry = false; } for (const line of lines) { if (LOG_LINE_PATTERN.test(line)) { // New log entry starts - flush the previous one flushEntry(); currentLogEntry.push(line); keepCurrentEntry = topicPattern.test(line); } else { // Continuation line (like "- `ERROR: ...`") currentLogEntry.push(line); } } // Flush the last entry flushEntry(); return result.join('\n'); } function main() { const args = process.argv.slice(2); const parsed = parseArgs(args); if (parsed === 'help') { showHelp(); return; } const { logTopic, filePath } = parsed; const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath); try { const content = fs.readFileSync(absolutePath, 'utf-8'); const filtered = filterLogByTopic(content, logTopic); fs.writeFileSync(absolutePath, filtered, 'utf-8'); console.log(`Filtered log file to only include [${logTopic}] entries: ${absolutePath}`); } catch (error) { const err = error as NodeJS.ErrnoException; if (err.code === 'ENOENT') { console.error(`Failed to read log file "${absolutePath}": file does not exist.`); } else if (err.code === 'EACCES' || err.code === 'EPERM') { console.error(`Permission denied while accessing log file "${absolutePath}".`); } else { console.error(`Failed to process log file "${absolutePath}": ${err.message ?? err}`); } process.exitCode = 1; } } main(); ================================================ FILE: script/compareStestAlternativeRuns.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { AssertionError } from 'assert'; import { execFile } from 'child_process'; import { promises as fs } from 'fs'; import * as path from 'path'; /** * An entry from `baseline.json`. */ interface BaselineTestResult { /** Test name */ name: string; score: number; passCount: number; failCount: number; contentFilterCount: number; attributes: (Record & { ['CompScore1']: number | undefined } & { ['CompScore2']: number | undefined } & { ['CompScore3']: number | undefined }); } enum SignalKind { OldFormat = 'OldFormat', MustHave = 'MustHave', NiceToHave = 'NiceToHave', BadSuggestions = 'BadSuggestions', Other = 'Other', } namespace SignalKind { export function getFromTestName(testName: string): SignalKind | undefined { const signalKindRe = `^\\[(${Object.values(SignalKind).join('|')})\\]`; const signalKind = testName.match(signalKindRe); if (signalKind) { return Object.values(SignalKind).includes(signalKind[1] as SignalKind) ? signalKind[1] as SignalKind : undefined; } } } interface TestResult { /** unflavored */ name: string; signalKind: SignalKind | undefined; testResults: BaselineTestResult[]; compScore1: number | undefined; compScore2: number | undefined; compScore3: number | undefined; } const regexForProviderName = / \(\[(([a-zA-Z0-9\-])+)\]\)/; const DEFAULT_PROVIDER_NAME = 'Default Provider'; function getFlavor(testResult: BaselineTestResult): string { const match = testResult.name.match(regexForProviderName); if (match) { switch (match[1]) { case 'prodFineTunedModel': return 'NES'; case 'prodFineTunedModelWithSummarizedDocument': return 'NES-summ'; case 'speculativeEditingInlineEditProvider': return 'SpecEdit'; default: return match[1]; } } else { return DEFAULT_PROVIDER_NAME; } } function computeTestResultsFromBaseline(baseline: BaselineTestResult[]): TestResult[] { const nesTestsWithFlavor = baseline.filter((currentBaselineTestResult) => currentBaselineTestResult.name.startsWith('NES ') || (currentBaselineTestResult.name.startsWith('InlineEdit') && currentBaselineTestResult.name.includes('])'))); const fullNameToTestName = (fullName: string) => { const indexOfSuiteTestNameSplit = fullName.indexOf(' - '); const testName = fullName.slice(indexOfSuiteTestNameSplit + 3); if (testName === undefined) { throw new AssertionError({ message: `does not follow the expected pattern: ${fullName}` }); } return testName; }; const testNameToResults = new Map(); for (const nesTest of nesTestsWithFlavor) { const testName = fullNameToTestName(nesTest.name); const baselineTestResults = testNameToResults.get(testName) ?? []; baselineTestResults.push(nesTest); testNameToResults.set(testName, baselineTestResults); } const sortedTestNameToFlavor = Array.from(testNameToResults.entries()); sortedTestNameToFlavor.sort((a, b) => { const aTestName = a[0]; const bTestName = b[0]; return aTestName.localeCompare(bTestName); }); return sortedTestNameToFlavor.map(([testName, baselineTestResults]) => { return { name: testName, signalKind: SignalKind.getFromTestName(testName), testResults: baselineTestResults, compScore1: baselineTestResults[0]?.attributes?.CompScore1 as number | undefined, compScore2: baselineTestResults[0]?.attributes?.CompScore2 as number | undefined, compScore3: baselineTestResults[0]?.attributes?.CompScore3 as number | undefined, } satisfies TestResult; }); } function formatAsBold(text: string) { return `${text} *`; } function formatAsColored(text: string, color: 'green' | 'violet' | 'red' | undefined) { if (!color) { return text; } const colorMap = { 'green': 32, 'red': 31, 'violet': 35, }; return `\x1b[${colorMap[color]}m${text}\x1b[0m`; } // For BadSuggestion tests, a score > 0 is considered a pass, otherwise a fail function isBadSuggestionPassed(score: number): boolean { return score > 0; } // Format pass ratio as a percentage string function formatPassRatio(passed: number, total: number): string { if (total === 0) { return '0.00%'; } return `${((passed / total) * 100).toFixed(2)}%`; } type TestScoreByFlavor = Record; type AggregatedTest = { test: string; scores: TestScoreByFlavor; signalKind?: SignalKind }; function printTable(data: AggregatedTest[], { compare, useColoredOutput, filterProviders, omitEqual }: { compare: boolean; useColoredOutput: boolean; filterProviders?: string[]; omitEqual: boolean }) { const providers = Array.from(new Set(data.flatMap(d => Object.keys(d.scores)))); const filteredProviders = filterProviders ? providers.filter(provider => filterProviders.includes(provider.toLocaleLowerCase())) : providers; const aggregatedTestsBySignalKind = data.reduce((acc: Record, item) => { const group = item.signalKind ?? SignalKind.Other; if (!acc[group]) { acc[group] = []; } acc[group].push(item); return acc; }, {} as Record); const tableData: Record[] = []; const totalScoreByProvider: Record = {}; const oldTotalScoreByProvider: Record = {}; // Track pass/fail counts for BadSuggestion tests const badSuggestionPassedByProvider: Record = {}; const badSuggestionTotalByProvider: Record = {}; const oldBadSuggestionPassedByProvider: Record = {}; for (const provider of filteredProviders) { totalScoreByProvider[provider] = 0; oldTotalScoreByProvider[provider] = 0; badSuggestionPassedByProvider[provider] = 0; badSuggestionTotalByProvider[provider] = 0; oldBadSuggestionPassedByProvider[provider] = 0; } // Iterate over each signal kind for (const [signalKind, tests] of Object.entries(aggregatedTestsBySignalKind)) { // add header tableData.push({ 'Test Name': `=== ${signalKind} ===` }); const totalByProviderForSignalKind: Record = {}; const oldTotalByProviderForSignalKind: Record = {}; // Track pass/fail counts for BadSuggestion tests within this signal kind const badSuggestionPassedByProviderForSignalKind: Record = {}; const badSuggestionTotalByProviderForSignalKind: Record = {}; const oldBadSuggestionPassedByProviderForSignalKind: Record = {}; for (const provider of filteredProviders) { totalByProviderForSignalKind[provider] = 0; oldTotalByProviderForSignalKind[provider] = 0; badSuggestionPassedByProviderForSignalKind[provider] = 0; badSuggestionTotalByProviderForSignalKind[provider] = 0; oldBadSuggestionPassedByProviderForSignalKind[provider] = 0; } const isBadSuggestionCategory = signalKind === SignalKind.BadSuggestions; for (const test of tests) { const scores = filteredProviders.map(provider => { const score = test.scores[provider]; const oldScore = typeof score === 'object' ? score.oldScore : undefined; const numericScore = typeof score === 'object' ? score.newScore : score ?? 0; // Handle BadSuggestion scores differently if (isBadSuggestionCategory) { badSuggestionTotalByProvider[provider]++; badSuggestionTotalByProviderForSignalKind[provider]++; if (isBadSuggestionPassed(numericScore)) { badSuggestionPassedByProvider[provider]++; badSuggestionPassedByProviderForSignalKind[provider]++; } if (oldScore !== undefined) { if (isBadSuggestionPassed(oldScore)) { oldBadSuggestionPassedByProvider[provider]++; oldBadSuggestionPassedByProviderForSignalKind[provider]++; } } } else { // Regular handling for non-BadSuggestion tests totalByProviderForSignalKind[provider] += numericScore; oldTotalScoreByProvider[provider] += oldScore ?? 0; totalScoreByProvider[provider] += numericScore; oldTotalByProviderForSignalKind[provider] += oldScore ?? 0; } return numericScore; }); const maxScore = Math.max(...scores); const minScore = Math.min(...scores); const areAllScoresEqual = maxScore === minScore; if (omitEqual && areAllScoresEqual) { continue; } const resultRow: Record = { 'Test Name': test.test }; for (let i = 0; i < filteredProviders.length; i++) { const provider = filteredProviders[i]; const rawScore = test.scores[provider]; const score = scores[i]; let formattedScore: string; if (isBadSuggestionCategory) { // For BadSuggestion, show "Pass" or "Fail" instead of score formattedScore = isBadSuggestionPassed(score) ? 'Pass' : 'Fail'; if (compare && typeof rawScore === 'object') { const oldResult = isBadSuggestionPassed(rawScore.oldScore) ? 'Pass' : 'Fail'; const newResult = isBadSuggestionPassed(rawScore.newScore) ? 'Pass' : 'Fail'; if (oldResult !== newResult) { const color = useColoredOutput ? (oldResult === 'Fail' && newResult === 'Pass' ? 'green' : 'red') : undefined; formattedScore = formatAsColored(`${oldResult} -> ${newResult}`, color); } } } else { // Regular formatting for non-BadSuggestion tests formattedScore = score.toFixed(2); if (compare && typeof rawScore === 'object' && rawScore.oldScore !== rawScore.newScore) { const color = useColoredOutput ? (rawScore.newScore > rawScore.oldScore ? 'green' : 'red') : undefined; formattedScore = formatAsColored(`${rawScore.oldScore.toFixed(2)} -> ${rawScore.newScore.toFixed(2)}`, color); } else if (maxScore - score < 0.001 && !areAllScoresEqual) { formattedScore = formatAsBold(formattedScore); } } resultRow[provider] = typeof rawScore === 'undefined' ? '-' : formattedScore; } tableData.push(resultRow); } // Add subtotal for signal kind const subtotalRow: Record = { 'Test Name': `${signalKind} Subtotal (${tests.length} tests)` }; for (const provider of filteredProviders) { if (isBadSuggestionCategory) { // For BadSuggestion, show pass ratio const passedTests = badSuggestionPassedByProviderForSignalKind[provider]; const totalTests = badSuggestionTotalByProviderForSignalKind[provider]; const passRatio = formatPassRatio(passedTests, totalTests); if (compare) { const oldPassedTests = oldBadSuggestionPassedByProviderForSignalKind[provider]; const oldPassRatio = formatPassRatio(oldPassedTests, totalTests); if (oldPassedTests !== passedTests) { const color = useColoredOutput ? (passedTests > oldPassedTests ? 'green' : 'red') : undefined; subtotalRow[provider] = formatAsColored(`${oldPassRatio} -> ${passRatio}`, color); } else { subtotalRow[provider] = passRatio; } } else { subtotalRow[provider] = passRatio; } } else { // Regular handling for non-BadSuggestion categories const oldSubTotal = oldTotalByProviderForSignalKind[provider]; const subTotal = totalByProviderForSignalKind[provider]; if (compare && Math.abs(oldSubTotal - subTotal) > 0.001 && !provider.startsWith('Comp')) { const rawOut = `${oldSubTotal.toFixed(2)} -> ${subTotal.toFixed(2)}`; const color = useColoredOutput ? (oldSubTotal < subTotal ? 'green' : 'red') : undefined; subtotalRow[provider] = formatAsColored(rawOut, color); } else { subtotalRow[provider] = subTotal.toFixed(2); } } } tableData.push(subtotalRow, { 'Test Name': '' }); } // Add total (don't include BadSuggestion in the grand total) const totalRow: Record = { 'Test Name': 'Grand Total (excluding BadSuggestions)' }; for (const provider of filteredProviders) { const oldTotal = oldTotalScoreByProvider[provider]; const total = totalScoreByProvider[provider]; if (compare && Math.abs(oldTotal - total) > 0.001 && !provider.startsWith('Comp')) { const rawOut = `${oldTotal.toFixed(2)} -> ${total.toFixed(2)}`; const color = useColoredOutput ? (oldTotal < total ? 'green' : 'red') : undefined; totalRow[provider] = formatAsColored(rawOut, color); } else { totalRow[provider] = total.toFixed(2); } } tableData.push(totalRow); // Add BadSuggestion aggregate pass ratio const badSuggestionRow: Record = { 'Test Name': 'BadSuggestion Pass Ratio' }; for (const provider of filteredProviders) { const passedTests = badSuggestionPassedByProvider[provider]; const totalTests = badSuggestionTotalByProvider[provider]; const passRatio = formatPassRatio(passedTests, totalTests); if (compare && totalTests > 0) { const oldPassedTests = oldBadSuggestionPassedByProvider[provider]; const oldPassRatio = formatPassRatio(oldPassedTests, totalTests); if (oldPassedTests !== passedTests) { const color = useColoredOutput ? (passedTests > oldPassedTests ? 'green' : 'red') : undefined; badSuggestionRow[provider] = formatAsColored(`${oldPassRatio} -> ${passRatio}`, color); } else { badSuggestionRow[provider] = passRatio; } } else { badSuggestionRow[provider] = passRatio; } } tableData.push(badSuggestionRow); console.table(tableData); } const DEFAULT_BASELINE_JSON_PATH = path.join(__dirname, '../test/simulation/baseline.json'); const DEFAULT_BASELINE_OLD_JSON_PATH = path.join(__dirname, '../test/simulation/baseline.old.json'); async function main() { const args = process.argv.slice(2); const compare = args.includes('--compare'); const upgradeBaselineOldJson = args.includes('--upgrade-old-baseline'); const useColoredOutput = args.includes('--color'); const omitEqual = args.includes('--omit-equal'); const filterArg = args.find(arg => arg.startsWith('--filter=')); const filterProviders = filterArg ? filterArg.split('=')[1].split(',').map(s => s.toLocaleLowerCase()) : undefined; const externalBaselineArg = args.find(arg => arg.startsWith('--external-baseline=')); const externalBaselinePath = externalBaselineArg ? externalBaselineArg.split('=')[1] : undefined; // Determine baseline paths const BASELINE_JSON_PATH = externalBaselinePath ? path.resolve(externalBaselinePath) : DEFAULT_BASELINE_JSON_PATH; const BASELINE_OLD_JSON_PATH = path.join(path.dirname(BASELINE_JSON_PATH), 'baseline.old.json'); let baselineJson: string; try { baselineJson = await fs.readFile(BASELINE_JSON_PATH, 'utf8'); } catch (e: unknown) { console.error('Failed to read baseline.json'); throw e; } let baseline: BaselineTestResult[]; try { baseline = JSON.parse(baselineJson) as BaselineTestResult[]; } catch (e: unknown) { console.error('Failed to parse baseline.json'); throw e; } if (upgradeBaselineOldJson) { const baselineJsonContentsFromHEAD = await new Promise((resolve, reject) => { execFile('git', ['show', `HEAD:${path.relative(process.cwd(), BASELINE_JSON_PATH)}`], (error: Error | null, stdout: string) => { if (error) { reject(error); return; } resolve(stdout); }); }); await fs.writeFile(BASELINE_OLD_JSON_PATH, baselineJsonContentsFromHEAD); } let oldBaseline: BaselineTestResult[] | undefined; if (compare) { let oldBaselineJson: string | undefined; try { oldBaselineJson = await fs.readFile(BASELINE_OLD_JSON_PATH, 'utf8'); } catch (e: unknown) { console.error('Failed to read baseline.json'); throw e; } try { oldBaseline = JSON.parse(oldBaselineJson) as BaselineTestResult[]; } catch (e: unknown) { console.error('Failed to parse baseline.json'); throw e; } } const testResults = computeTestResultsFromBaseline(baseline); const oldTestResults = compare && oldBaseline ? computeTestResultsFromBaseline(oldBaseline) : undefined; const testNameToOldScoresByFlavor = oldTestResults?.reduce((acc: Record>, testResult) => { acc[testResult.name] = testResult.testResults.reduce((acc, testResult) => { acc[getFlavor(testResult)] = testResult.score; return acc; }, { 'Comp1': testResult.compScore1, 'Comp2': testResult.compScore2, 'Comp3': testResult.compScore3 } as Record); return acc; }, {}) ?? {}; const result = testResults.map(testResult => { const oldScoresByFlavor = testNameToOldScoresByFlavor[testResult.name] || {}; const scores = testResult.testResults.reduce((acc: TestScoreByFlavor, testResult) => { const flavor = getFlavor(testResult); const newScore = testResult.score; const oldScore = oldScoresByFlavor[flavor]; acc[flavor] = oldScore === undefined ? newScore : { oldScore, newScore }; return acc; }, { 'Comp1': testResult.compScore1, 'Comp2': testResult.compScore2, 'Comp3': testResult.compScore3 }); return { test: testResult.name, signalKind: testResult.signalKind, scores, }; }); printTable(result, { compare, useColoredOutput, filterProviders, omitEqual }); } main(); ================================================ FILE: script/electron/simulationWorkbench.css ================================================ .testListMenu { position: absolute; z-index: 1000; border: 1px solid black; } .testListMenu .hidden { display: none; } .clickable { cursor: pointer; } .testRun { cursor: initial; padding-left: 20px; padding-bottom: 30px; display: flex; flex-direction: row; justify-content: space-between; gap: 10px; } .foldingBar { width: 30px; display: flex; align-items: center; justify-content: center; background-color: rgb(228, 227, 227); } .foldingBar:hover { background-color: rgb(216, 216, 216); } .content { width: 100%; } .request-container .title { cursor: pointer; } .toolbar { padding: 5px; } .toolbar .dropdown { display: inline-block; margin-right: 10px; } .toolbar input { margin-left: 5px; } .toolbar input.nruns { width: 20px; } .toolbar button.button { margin-left: 5px; } .toolbar div.useCache { display: inline-block; vertical-align: middle; } .go-to-mode-button { float: right; font-size: 13px !important; } .external-toolbar-filter { display: inline; margin-left: 10px; margin-right: 10px; } .external-toolbar-filter .external-toolbar-dropdown { margin-left: 10px; margin-right: 10px; } .tests-renderer { font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", "Segoe UI", Calibri, Helvetica, Arial, sans-serif; font-size: 12px; } .test-renderer { margin-bottom: 1px; padding-top: 2px; padding-bottom: 2px; } .test-renderer .runner-status { float: left; width: 15px; height: 15px; } .test-renderer .runner-status.running::before { content: '🏃'; } .test-renderer .runner-status.running { background-color: #5e740b; } .test-renderer .runner-status.pending::before { content: '⌛'; } .test-renderer .runner-status.pending { background-color: #d2e0e6; } .test-renderer .runner-status.finished::before { content: '🏁'; } .test-renderer .runner-status.cancelled::before { content: '⏹️'; } .test-renderer .runner-status.skipped::before { content: '⏭️'; } .test-renderer .runner-status.not-run::before { content: '🔘'; } .test-renderer .test-score { float: left; padding-right: 5px; text-align: right; border-right: #000 2px; font-weight: bold; white-space: pre; font-family: monospace; } .test-renderer .suite-title { float: left; font-weight: bold; margin-right: 4px; } .test-renderer .test-title { float: left; font-weight: bold; } .test-renderer .test-title .test-duration { font-size: 80%; opacity: 0.8; } .test-runs-container { border-top: 2px solid black; } .test-renderer .test-run-renderer.fail { background-color: #f2dede; } .request-container { margin-top: 5px; margin-bottom: 10px; } .request-container .title { font-family: Arial, "Segoe UI"; font-size: 1em; font-weight: bold; } .request-container .reply .monaco-editor-background, .request-container .reply .monaco-editor, .request-container .reply .monaco-editor .margin { background-color: rgb(218, 255, 234); } .file-editor-container { margin-top: 10px; margin-left: 20px; width: 95%; border: none; border-radius: 2pt; box-shadow: 0 0 0 2pt #0000004b; } .file-editor-draggable-border { height: 5px; width: 95%; background: #ccc; cursor: row-resize; margin-left: 20px; } .file-editor-draggable-border:hover { background: #4d90fe; } .step-title { font-family: Arial, "Segoe UI"; font-size: 1.1em; font-weight: bold; margin-top: 5px; } .error-comparison { font-family: Arial, 'Segoe UI'; margin-left: 30px; margin-top: 10px; margin-bottom: 10px; } .error-comparison .title { font-size: 1em; font-weight: bold; } .error-comparison .category { font-weight: bold; } .diagnostics-comparison { font-size: 1em; } .diagnostics-comparison { margin-left: 30px; } .step-query { font-family: Monaco, 'Cascadia Code', monospace; white-space: pre; word-break: break-word; text-wrap: wrap; } .assertion-error { margin-left: 20px; padding-bottom: 10px; } .monaco-editor .step-range-highlight { background-color: rgba(235, 219, 0, 0.2); } .monaco-editor .dec-diagnostic { outline: 1px solid red; } .monaco-editor .dec-diagnostic-invalid-range { outline: 2px solid red; background-color: #f004; } .monaco-editor .cursor { visibility: inherit !important; } /* .step-markdown { font-family: Arial; width: 400px; margin-left: 30px } .step-markdown code { font-family: Monaco; color: red; } */ .scorecard-container { margin-top: 10px; margin-bottom: 10px; } .scorecard-title { font-size: 1.25em; } .scorecard-table { border: 1px solid black; border-collapse: collapse; } .scorecard-table td, .scorecard-table th { border: 1px solid black; padding: 4px; } .tooltip .tooltiptext { visibility: hidden; width: auto; background-color: #555; color: #fff; text-align: center; border-radius: 3px; padding: 3px 5px; position: absolute; z-index: 1; opacity: 0; transition: opacity 0.3s; } .tooltip:hover .tooltiptext { visibility: visible; opacity: 1; } .tooltip .tooltiptext.top { bottom: 125%; left: 50%; margin-left: -60px; } .tooltip .tooltiptext.bottom { top: 125%; left: 50%; margin-left: -60px; } .tooltip .tooltiptext.left { right: 50%; margin-right: -60px; } .tooltip .tooltiptext.right { left: 50%; margin-left: -60px; } .context-menu { position: fixed; z-index: 1000; background-color: #fff; border: 1px solid #ccc; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); padding: 5px 0; min-width: 150px; display: none; } .fade-out-background { animation: fadeOutBackground 5s ease-out forwards; } @keyframes fadeOutBackground { from { background-color: lightgreen; } to { background-color: transparent; } } ================================================ FILE: script/electron/simulationWorkbench.html ================================================ Simulation Workbench ================================================ FILE: script/electron/simulationWorkbenchMain.js ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ //@ts-check const electron = require('electron'); const child_process = require('child_process'); const path = require('path'); const app = electron.app; /** * @type {Electron.BrowserWindow | null} * The main window of the Electron application. */ let mainWindow = null; function createWindow() { const { width, height } = electron.screen.getPrimaryDisplay().workAreaSize; mainWindow = new electron.BrowserWindow({ width: width * 0.75, height: height, webPreferences: { nodeIntegration: true, contextIsolation: false } }); mainWindow.loadURL(`file://${__dirname}/simulationWorkbench.html`); mainWindow.on('closed', function () { mainWindow = null; }); } app.on('ready', () => { if (process.argv.includes('--help')) { console.log(`Options: --run-dir=DIRNAME Provide the run output directory name, e.g., 'out-20231201-151346'. --grep=STRING Pre-populates simulation workbench 'grep' input box.`); app.quit(); } registerListeners(); createWindow(); }); app.on('window-all-closed', function () { if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', function () { if (mainWindow === null) { createWindow(); } }); // change to configure logging, e.g., to `console.debug` const log = { debug: (..._args) => { } }; function registerListeners() { /** @type {Map} */ const spawnedProcesses = new Map(); /** * Spawns a new child process and sets up listeners for its stdout, stderr, and exit events. * * @param {Object} event - The event object from the Electron IPC. * @param {Object} options - The options for the child process. * @param {string} options.id - The unique identifier for the child process created by the renderer process. * @param {Array} options.processArgs - The arguments to pass to the child process. */ function spawnProcess(event, { id, processArgs }) { log.debug(`main process: spawn-process (id: ${id}, processArgs: ${JSON.stringify(processArgs)})`); const child = child_process.spawn( 'node', [path.join(__dirname, '../../dist', 'simulationMain.js'), ...processArgs], { stdio: 'pipe' } ); if (child.pid) { child.stdout.setEncoding('utf8'); child.stdout.on('data', (data) => { log.debug(`main process: stdout: ${data.toString()}`); event.sender.send('stdout-data', { id, data }); }); child.stderr.setEncoding('utf8'); child.stderr.on('data', (data) => { log.debug(`main process: stderr: ${data.toString()}`); event.sender.send('stderr-data', { id, data }); }); child.on('exit', (code) => { log.debug('main process: ' + JSON.stringify(code, null, '\t')); spawnedProcesses.delete(id); event.sender.send('process-exit', { id, code }); }); spawnedProcesses.set(id, child); } } electron.ipcMain.on('spawn-process', spawnProcess); electron.ipcMain.on('kill-process', (_event, { id }) => { spawnedProcesses.get(id)?.kill('SIGTERM'); spawnedProcesses.delete(id); }); electron.ipcMain.on('open-link', (_event, url) => { electron.shell.openExternal(url); }); electron.ipcMain.handle('processArgv', () => { return process.argv; }); } ================================================ FILE: script/eslintGitBlameReport/generateEslintIgnoreReport.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { spawnSync, SpawnSyncOptions } from 'child_process'; import { createHash } from 'crypto'; import { promises as fs } from 'fs'; import * as path from 'path'; interface ESLintMessage { ruleId: string | null; severity: number; message: string; line: number; column: number; } interface ESLintResult { filePath: string; messages: ESLintMessage[]; } interface CommitHandleCache { [commit: string]: string; } const owner = 'microsoft'; const repo = 'vscode-copilot-chat'; const repoRoot = path.resolve(__dirname, '../..'); const alternateRepoRoot = path.resolve(repoRoot, '..', 'vscode-copilot'); const lintCacheDir = path.join(repoRoot, '.lint-cache'); const lintOutputPath = path.join(lintCacheDir, 'eslint-output.json'); const commitHandleCachePath = path.join(lintCacheDir, 'commit-handles.json'); const failedHandleCommits = new Set(); const alternateRepoHandleCache = new Map(); let alternateRepoAvailability: boolean | undefined; void main().catch(error => { console.error(error instanceof Error ? error.message : error); process.exit(1); }); async function main(): Promise { await fs.mkdir(lintCacheDir, { recursive: true }); const { cacheKey, results } = await getLintResults(); const violatingFiles = collectViolations(results); if (!violatingFiles.size) { console.log('No ESLint violations detected.'); return; } const commitHandles = await loadCommitHandles(); let cacheDirty = false; const reportLines: string[] = []; for (const [file, messages] of violatingFiles) { const resolvedMessages: { message: ESLintMessage; username: string }[] = []; for (const message of messages) { const handle = await resolveHandleForMessage(file, message.line, commitHandles); if (handle.commit) { commitHandles[handle.commit] = handle.username; } cacheDirty = cacheDirty || handle.isNew; resolvedMessages.push({ message, username: handle.username }); } const uniqueHandles = new Set(resolvedMessages.map(entry => entry.username)); if (uniqueHandles.size === 1 && resolvedMessages.length) { const onlyHandle = resolvedMessages[0].username; reportLines.push(`- [ ] ${file} @${onlyHandle}`); } else { reportLines.push(`- [ ] ${file}`); for (const { message, username } of resolvedMessages) { reportLines.push(formatReportLine(message, username)); } } reportLines.push(''); } if (cacheDirty) { await fs.writeFile(commitHandleCachePath, JSON.stringify(commitHandles, null, 2), 'utf8'); } await updateEslintIgnores(Array.from(violatingFiles.keys())); console.log(reportLines.join('\n')); console.log(`Cached lint results key: ${cacheKey}`); } async function getLintResults(): Promise<{ cacheKey: string; results: ESLintResult[] }> { const gitHead = runGit(['rev-parse', 'HEAD']); const gitStatus = runGit(['status', '--porcelain']); const cacheKey = createHash('sha1').update(`${gitHead}\n${gitStatus}`).digest('hex'); const cacheFile = path.join(lintCacheDir, `${cacheKey}.json`); if (await fileExists(cacheFile)) { const cached = await fs.readFile(cacheFile, 'utf8'); return { cacheKey, results: JSON.parse(cached) as ESLintResult[] }; } await fs.rm(lintOutputPath, { force: true }); runLintCommand(); const lintOutput = await fs.readFile(lintOutputPath, 'utf8'); const parsed = JSON.parse(lintOutput) as ESLintResult[]; await fs.writeFile(cacheFile, JSON.stringify(parsed, null, 2), 'utf8'); return { cacheKey, results: parsed }; } function runLintCommand(): void { const cacheLocation = path.join(lintCacheDir, '.eslintcache'); const args = ['run', 'lint', '--', '--format', 'json', '--output-file', lintOutputPath, '--cache', '--cache-location', cacheLocation]; const result = spawnSync('npm', args, spawnOptions()); if (result.error) { throw result.error; } if (result.status !== 0 && result.status !== 1) { throw new Error(`npm run lint failed with exit code ${result.status ?? 'unknown'}`); } } function spawnOptions(): SpawnSyncOptions { return { cwd: repoRoot, stdio: 'inherit' }; } function collectViolations(results: ESLintResult[]): Map { const violations = new Map(); for (const result of results) { const relevantMessages = result.messages.filter(message => message.severity > 0); if (!relevantMessages.length) { continue; } const relativeFile = toPosixPath(path.relative(repoRoot, result.filePath)); const prefixed = relativeFile.startsWith('.') ? relativeFile : `./${relativeFile}`; violations.set(prefixed, relevantMessages); } return violations; } async function loadCommitHandles(): Promise { if (!(await fileExists(commitHandleCachePath))) { return {}; } const raw = await fs.readFile(commitHandleCachePath, 'utf8'); try { return JSON.parse(raw) as CommitHandleCache; } catch (error) { console.warn('Failed to parse commit handle cache, starting fresh.'); return {}; } } interface HandleResolution { commit?: string; username: string; isNew: boolean; } async function resolveHandleForMessage(file: string, line: number, cache: CommitHandleCache): Promise { let blameCommit: string | undefined; try { blameCommit = extractCommitHash(runGit(['blame', '--line-porcelain', '-L', `${line},${line}`, file])); } catch (error) { throw new Error(`Failed to run git blame for ${file}:${line}: ${error instanceof Error ? error.message : String(error)}`); } const blameHandle = await getHandleForCommit(blameCommit, cache); if (blameHandle && blameHandle.username !== 'kieferrm') { return { commit: blameCommit, username: blameHandle.username, isNew: blameHandle.isNew }; } if (blameHandle && blameHandle.username === 'kieferrm') { const alternateHandle = await resolveHandleFromAlternateRepo(file); if (alternateHandle) { return { username: alternateHandle, isNew: false }; } } let lastCommit: string | undefined; try { lastCommit = extractCommitHash(runGit(['log', '-n', '1', '--pretty=format:%H', '--', file])); } catch (error) { throw new Error(`Failed to find last change for ${file}: ${error instanceof Error ? error.message : String(error)}`); } const fallbackHandle = await getHandleForCommit(lastCommit, cache); if (fallbackHandle) { if (fallbackHandle.username === 'kieferrm') { const alternateHandle = await resolveHandleFromAlternateRepo(file); if (alternateHandle) { return { username: alternateHandle, isNew: false }; } } return { commit: lastCommit, username: fallbackHandle.username, isNew: fallbackHandle.isNew }; } return { username: 'kieferrm', isNew: false }; } interface CommitHandleLookup { username: string; isNew: boolean; } async function getHandleForCommit(commit: string | undefined, cache: CommitHandleCache): Promise { if (!commit) { return undefined; } if (cache[commit]) { return { username: cache[commit], isNew: false }; } if (failedHandleCommits.has(commit)) { return undefined; } let login: string | undefined; const env = { ...process.env, GH_PAGER: 'cat', GH_PROMPT_DISABLED: '1' }; const response = spawnSync('gh', ['api', `/repos/${owner}/${repo}/commits/${commit}`], { cwd: repoRoot, encoding: 'utf8', env }); if (response.status === 0 && response.stdout) { try { const data = JSON.parse(response.stdout); login = data.author?.login ?? data.committer?.login ?? data.commit?.author?.name; } catch (error) { console.warn(`Failed to parse GitHub API response for commit ${commit}`); } } else if (response.status !== 0) { const stderr = typeof response.stderr === 'string' ? response.stderr.trim() : ''; console.warn(`gh api commit ${commit} exited with code ${response.status}${stderr ? `: ${stderr}` : ''}`); } if (!login) { login = getHandleFromLocalGit(commit); } if (!login) { failedHandleCommits.add(commit); console.warn(`Unable to resolve GitHub handle for commit ${commit}`); return undefined; } const normalized = normalizeHandle(login); cache[commit] = normalized; return { username: normalized, isNew: true }; } function getHandleFromLocalGit(commit: string): string | undefined { try { const email = runGit(['show', '-s', '--format=%ae', commit]); const handleFromEmail = extractHandleFromEmail(email); if (handleFromEmail) { return handleFromEmail; } const author = runGit(['show', '-s', '--format=%an', commit]); return normalizePossibleHandle(author); } catch { return undefined; } } function extractHandleFromEmail(email: string): string | undefined { const noreplyPattern = /^(?:\d+\+)?([A-Za-z0-9-]+)@users\.noreply\.github\.com$/; const match = email.match(noreplyPattern); if (match) { return match[1]; } return undefined; } function normalizePossibleHandle(name: string): string | undefined { const normalized = name.trim(); if (!normalized || /\s/.test(normalized)) { return undefined; } return normalized; } function normalizeHandle(handle: string): string { return handle.startsWith('@') ? handle.substring(1) : handle; } function extractCommitHash(blameOutput: string): string | undefined { const firstLine = blameOutput.split('\n')[0]?.trim(); if (!firstLine) { return undefined; } const commit = firstLine.split(' ')[0]; if (!commit || /^[0]+$/.test(commit)) { return undefined; } return commit.startsWith('^') ? commit.substring(1) : commit; } function formatReportLine(message: ESLintMessage, handle: string): string { const rule = message.ruleId ?? ''; const column = message.column ?? 0; const line = `${message.line}:${column}`; const components = [` - [ ] ${line}`]; if (rule) { components.push(rule); } if (handle) { components.push(`@${handle}`); } return components.join(' '); } async function updateEslintIgnores(files: string[]): Promise { if (!files.length) { return; } const configPath = path.join(repoRoot, 'ignores.md'); const nextContent = files.map(file => `'${file}'`).join(',\n'); await fs.writeFile(configPath, nextContent, 'utf8'); } function toPosixPath(input: string): string { return input.split(path.sep).join('/'); } function runGit(args: string[]): string { return runGitCommand(repoRoot, args); } async function fileExists(filePath: string): Promise { try { await fs.stat(filePath); return true; } catch { return false; } } async function resolveHandleFromAlternateRepo(file: string): Promise { if (alternateRepoHandleCache.has(file)) { const cached = alternateRepoHandleCache.get(file); return cached ?? undefined; } if (!(await hasAlternateRepo())) { alternateRepoHandleCache.set(file, null); return undefined; } const relativeFile = file.startsWith('./') ? file.substring(2) : file; const fileForGit = relativeFile.split('/').join(path.sep); const absolutePath = path.join(alternateRepoRoot, fileForGit); if (!(await fileExists(absolutePath))) { alternateRepoHandleCache.set(file, null); return undefined; } try { const lastCommit = runGitCommand(alternateRepoRoot, ['log', '-n', '1', '--pretty=format:%H', '--', fileForGit]); if (!lastCommit) { alternateRepoHandleCache.set(file, null); return undefined; } const email = runGitCommand(alternateRepoRoot, ['show', '-s', '--format=%ae', lastCommit]); const handleFromEmail = extractHandleFromEmail(email); let resolvedHandle = handleFromEmail ? normalizeHandle(handleFromEmail) : undefined; if (!resolvedHandle) { const author = runGitCommand(alternateRepoRoot, ['show', '-s', '--format=%an', lastCommit]); const possibleHandle = normalizePossibleHandle(author); if (possibleHandle) { resolvedHandle = normalizeHandle(possibleHandle); } } if (resolvedHandle) { alternateRepoHandleCache.set(file, resolvedHandle); return resolvedHandle; } } catch (error) { console.warn(`Failed to resolve alternate repo handle for ${file}${error instanceof Error ? `: ${error.message}` : ''}`); } alternateRepoHandleCache.set(file, null); return undefined; } async function hasAlternateRepo(): Promise { if (alternateRepoAvailability !== undefined) { return alternateRepoAvailability; } try { const stats = await fs.stat(alternateRepoRoot); alternateRepoAvailability = stats.isDirectory(); } catch { alternateRepoAvailability = false; } return alternateRepoAvailability; } function runGitCommand(cwd: string, args: string[]): string { const result = spawnSync('git', args, { cwd, encoding: 'utf8' }); if (result.status !== 0) { throw new Error(`git ${args.join(' ')} failed: ${result.stderr || result.stdout}`); } return (result.stdout ?? '').trim(); } ================================================ FILE: script/logRecordingTypes.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ /// !!! NOTE: copied over from src/platform/workspaceRecorder/common/workspaceLog.ts export type LogDocumentId = number; export type LogEntry = | HeaderLogEntry | ApplicationStartLogEntry | DocumentSetContentLogEntry | DocumentStoreContentLogEntry | DocumentRestoreContentLogEntry | DocumentOpenedLogEntry | DocumentClosedLogEntry | DocumentChangedLogEntry | DocumentFocusChangedLogEntry | DocumentSelectionChangedLogEntry | DocumentEncounteredLogEntry | MetaLogEntry | BookmarkLogEntry | DocumentEventLogEntry | EventLogEntry; export type DocumentLogEntry = { id: LogDocumentId; time: number }; export namespace DocumentLogEntry { export function is(entry: unknown): entry is DocumentLogEntry { return !!entry && typeof entry === 'object' && 'id' in entry && 'time' in entry; } } /** First entry of the log */ export type HeaderLogEntry = { documentType: "workspaceRecording@1.0"; kind: 'header'; repoRootUri: string; time: number; uuid: string }; export type ApplicationStartLogEntry = { kind: 'applicationStart'; time: number }; export type DocumentSetContentLogEntry = DocumentLogEntry & { kind: 'setContent'; content: string; /* if undefined, is 0 */ v: number | undefined }; export type DocumentStoreContentLogEntry = DocumentLogEntry & { kind: 'storeContent'; contentId: string; /* if undefined, is 0 */ v: number | undefined }; /** Can only restore from a content id set by any previous store content log entry */ export type DocumentRestoreContentLogEntry = DocumentLogEntry & { kind: 'restoreContent'; contentId: string; /* if undefined, is 0 */ v: number | undefined }; export type DocumentOpenedLogEntry = DocumentLogEntry & { kind: 'opened' }; export type DocumentClosedLogEntry = DocumentLogEntry & { kind: 'closed' }; export type DocumentChangedLogEntry = DocumentLogEntry & { kind: 'changed'; edit: ISerializedEdit; v: number }; export type DocumentFocusChangedLogEntry = DocumentLogEntry & { kind: 'focused' }; export type DocumentSelectionChangedLogEntry = DocumentLogEntry & { kind: 'selectionChanged'; selection: ISerializedOffsetRange[] }; export type DocumentEncounteredLogEntry = DocumentLogEntry & { kind: 'documentEncountered'; relativePath: string }; export type DocumentEventLogEntry = DocumentLogEntry & { kind: 'documentEvent'; data: unknown }; export type EventLogEntry = { kind: 'event'; time: number; data: unknown }; export type MetaLogEntry = { kind: 'meta'; data: unknown | { repoRootUri: string } }; export type BookmarkLogEntry = { kind: 'bookmark'; time: number }; // Edit functions export type ISerializedOffsetRange = [start: number, endEx: number]; export type ISerializedEdit = [start: number, endEx: number, text: string][]; ================================================ FILE: script/postinstall.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; import * as path from 'path'; import { compressTikToken } from './build/compressTikToken'; import { copyStaticAssets } from './build/copyStaticAssets'; export interface ITreeSitterGrammar { name: string; /** * A custom .wasm filename if the grammar node module doesn't follow the standard naming convention */ filename?: string; /** * The path where we should spawn `tree-sitter build-wasm` */ projectPath?: string; } const treeSitterGrammars: ITreeSitterGrammar[] = [ { name: 'tree-sitter-c-sharp', filename: 'tree-sitter-c_sharp.wasm' // non-standard filename }, { name: 'tree-sitter-cpp', }, { name: 'tree-sitter-go', }, { name: 'tree-sitter-javascript', // Also includes jsx support }, { name: 'tree-sitter-python', }, { name: 'tree-sitter-ruby', }, { name: 'tree-sitter-typescript', projectPath: 'tree-sitter-typescript/typescript', // non-standard path }, { name: 'tree-sitter-tsx', projectPath: 'tree-sitter-typescript/tsx', // non-standard path }, { name: 'tree-sitter-java', }, { name: 'tree-sitter-rust', }, { name: 'tree-sitter-php' } ]; const REPO_ROOT = path.join(__dirname, '..'); /** * @github/copilot/sdk/index.js depends on @github/copilot/worker/*.js files. * We need to copy these files into the sdk directory to ensure they are available at runtime. */ async function copyCopilotCliWorkerFiles() { const sourceDir = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot', 'worker'); const targetDir = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot', 'sdk', 'worker'); await copyCopilotCLIFolders(sourceDir, targetDir); } async function copyCopilotCliSharpFiles() { const sourceDir = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot', 'sharp'); const targetDir = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot', 'sdk', 'sharp'); await copyCopilotCLIFolders(sourceDir, targetDir); } async function copyCopilotCliDefinitionFiles() { const sourceDir = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot', 'definitions'); const targetDir = path.join(REPO_ROOT, 'node_modules', '@github', 'copilot', 'sdk', 'definitions'); await copyCopilotCLIFolders(sourceDir, targetDir); } async function copyCopilotCLIFolders(sourceDir: string, targetDir: string) { await fs.promises.rm(targetDir, { recursive: true, force: true }); await fs.promises.mkdir(targetDir, { recursive: true }); await fs.promises.cp(sourceDir, targetDir, { recursive: true, force: true }); } async function main() { await fs.promises.mkdir(path.join(REPO_ROOT, '.build'), { recursive: true }); const vendoredTiktokenFiles = ['src/platform/tokenizer/node/cl100k_base.tiktoken', 'src/platform/tokenizer/node/o200k_base.tiktoken']; for (const tokens of vendoredTiktokenFiles) { await compressTikToken(tokens, `dist/${path.basename(tokens)}`); } // copy static assets to dist await copyStaticAssets([ ...treeSitterGrammars.map(grammar => `node_modules/@vscode/tree-sitter-wasm/wasm/${grammar.name}.wasm`), 'node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm', 'node_modules/@github/blackbird-external-ingest-utils/pkg/nodejs/external_ingest_utils_bg.wasm', ], 'dist'); await copyCopilotCliWorkerFiles(); await copyCopilotCliSharpFiles(); await copyCopilotCliDefinitionFiles(); // Check if the base cache file exists const baseCachePath = path.join('test', 'simulation', 'cache', 'base.sqlite'); if (!fs.existsSync(baseCachePath)) { throw new Error(`Base cache file does not exist at ${baseCachePath}. Please ensure that you have git lfs installed and initialized before the repository is cloned.`); } await copyStaticAssets([ `node_modules/@anthropic-ai/claude-agent-sdk/cli.js`, ], 'dist'); } main(); ================================================ FILE: script/scoredEditsReconciler.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { execSync } from 'child_process'; import * as fs from 'fs'; import minimist from 'minimist'; import * as path from 'path'; async function main() { const args = minimist(process.argv.slice(2)); const filePath = args.file; const list = args.list; const reconcileUsingGit = args.a || args.auto; const filesWithMergeConflicts = await scoredEditsWithMergeConflicts(); if (list) { console.log(filesWithMergeConflicts.join('\n')); return; } if (filePath) { try { const resolvedFileContents = await resolveMergeConflictFromFile(filePath); await fs.promises.writeFile(filePath, resolvedFileContents, 'utf8'); } catch (e: unknown) { throw e; } return; } if (reconcileUsingGit) { try { await Promise.all(filesWithMergeConflicts.map(async (filePath) => { const resolvedFileContents = await resolveMergeConflictFromFile(filePath); return fs.promises.writeFile(filePath, resolvedFileContents); })); return; } catch (e: unknown) { throw e; } } console.log(` Usage: scoredEditReconciler [options] Options: -a, --auto Reconcile merge conflicts automatically by finding files with merge conflicts using git --file Path to the file to resolve merge conflicts --list List files with merge conflicts --help Show help `.trim()); } async function scoredEditsWithMergeConflicts(): Promise /* paths */ { const files = await findFilesWithMergeConflicts(); return files.filter(file => file.endsWith('scoredEdits.w.json')); } async function findFilesWithMergeConflicts() { try { // Get files with merge conflicts using git command const gitOutput = execSync('git diff --name-only --diff-filter=U').toString(); // Split output into array of file paths const conflictFiles = gitOutput.split('\n').filter(file => file.trim().length > 0); return conflictFiles.map(file => path.resolve(file)); } catch (error) { console.error('Error finding files with merge conflicts:', error); return []; } } async function resolveMergeConflictFromFile(filePath: string) { const fileContents = await fs.promises.readFile(filePath, 'utf8'); return resolveMergeConflict(fileContents); } export function resolveMergeConflict(fileContents: string): string { const headFileContents = removeNonHeadSections(fileContents); const nonHeadFileContents = removeHeadSections(fileContents); const headFileAsObject = JSON.parse(headFileContents); const nonHeadfileAsObject = JSON.parse(nonHeadFileContents); if (JSON.stringify({ ...headFileAsObject, edits: [] }) !== JSON.stringify({ ...nonHeadfileAsObject, edits: [] })) { throw new Error('There seems to be merge conflict outside `edits` field which this script can resolve automatically.'); } const mergedEdits = [...headFileAsObject.edits]; for (const edit of nonHeadfileAsObject.edits) { if (!mergedEdits.some(headEdit => JSON.stringify(headEdit) === JSON.stringify(edit))) { mergedEdits.push(edit); } } const resolvedFileContents = JSON.stringify({ ...headFileAsObject, edits: mergedEdits }, null, '\t'); return resolvedFileContents; } function removeNonHeadSections(fileContents: string) { const lines = fileContents.split('\n'); const headLines = []; let insideNonHead = false; for (const line of lines) { if (line.startsWith('=======')) { insideNonHead = true; } else if (line.startsWith('>>>>>>>')) { insideNonHead = false; } else if (!insideNonHead && !line.startsWith('<<<<<<<')) { headLines.push(line); } } return headLines.join('\n'); } function removeHeadSections(fileContents: string) { const lines = fileContents.split('\n'); const nonHeadLines = []; let insideHead = false; for (const line of lines) { if (line.startsWith('<<<<<<<')) { insideHead = true; } else if (line.startsWith('=======')) { insideHead = false; } else if (!insideHead && !line.startsWith('>>>>>>>')) { nonHeadLines.push(line); } } return nonHeadLines.join('\n'); } main(); ================================================ FILE: script/setup/copySources.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; import * as path from 'path'; import { join } from 'path'; import * as ts from 'typescript'; const VS_ROOT = join(__dirname, '../../../vscode/src'); const TARGET = join(__dirname, '../../src/util/vs'); /** * Returns the absolute file path where the given file should be placed. */ function determineTargetPath(absoluteVSCodeFilePath: string): string { const vsRelative = path.relative(VS_ROOT, absoluteVSCodeFilePath); const segements = vsRelative.split(path.sep); if (segements[0] === 'typings' || segements[0] === 'vs') { segements.shift(); } return join(TARGET, segements.join(path.sep)); } /** * Returns the relative path of `importedFilePath` to `currentFilePath` in a format suitable for import statements. */ function createRelativeImportPath(currentFilePath: string, importedFilePath: string): string { const relativePath = path.relative(path.dirname(currentFilePath), importedFilePath).replaceAll('\\', '/'); const result = relativePath.startsWith('.') ? relativePath : './' + relativePath; return result.replace(/\.ts$/, ''); } async function doIt(filepaths: string[]) { try { await fs.promises.access(VS_ROOT); } catch { console.error(`❌ VS Code root not found at ${VS_ROOT}`); process.exit(1); } try { await fs.promises.rm(join(TARGET), { recursive: true }); } catch { // ignore } type Edit = ts.TextRange & { newText: string }; type File = { sourceFilePath: string; targetFilePath: string; contents: string }; type StackElement = { filepath: string; importTrajectory: string[] }; const seen = new Map(); // indexed by sourceFilePath const stack: StackElement[] = [...filepaths.map(p => ({ filepath: join(VS_ROOT, p), importTrajectory: [] }))]; while (stack.length > 0) { const stackElement = stack.pop()!; const importTrajectory = stackElement.importTrajectory.slice(0); importTrajectory.push(stackElement.filepath); let filepath = stackElement.filepath; if (seen.has(filepath)) { continue; } const edits: Edit[] = []; let source: string = ''; try { source = String(await fs.promises.readFile(filepath)); } catch (e) { try { // .ts doesn't exist, try, .d.ts filepath = filepath.replace(/\.ts$/, '.d.ts'); source = String(await fs.promises.readFile(filepath)); } catch (e) { console.error(`❌ Error reading file ${filepath}. Trajectory:\n${stackElement.importTrajectory.reverse().map(el => `- ${el}`).join('\n')}:`); throw e; } } const destinationFilePath = determineTargetPath(filepath); const info = ts.preProcessFile(source, true, true); for (const importedFile of info.importedFiles) { let absolutePath: string | undefined; if (importedFile.fileName.startsWith('.')) { absolutePath = join(filepath, '..', importedFile.fileName.replace(/\.js$/, '.ts')); } else if (importedFile.fileName.includes('/')) { absolutePath = join(VS_ROOT, importedFile.fileName.replace(/\.js$/, '.ts')); } if (absolutePath) { stack.push({ filepath: absolutePath, importTrajectory }); edits.push({ ...importedFile, newText: createRelativeImportPath(destinationFilePath, determineTargetPath(absolutePath)), }); } // console.log(`${filepath} << b.pos - a.pos)) { newSource = newSource.slice(0, edit.pos + 1) + edit.newText + newSource.slice(edit.end + 1); } newSource = '//!!! DO NOT modify, this file was COPIED from \'microsoft/vscode\'\n\n' + newSource; seen.set(filepath, { sourceFilePath: filepath, targetFilePath: destinationFilePath, contents: newSource }); } for (const [_, file] of seen) { const targetFilepath = file.targetFilePath; await fs.promises.mkdir(join(targetFilepath, '..'), { recursive: true }); await fs.promises.writeFile(targetFilepath, file.contents); } console.log(`✅ done, copied ${filepaths.length} files and ${seen.size - filepaths.length} dependencies`); } (async function () { try { await doIt([ // ******************************************** // add modules from `base` here and // run `npx tsx script/setup/copySources.ts` // ******************************************** 'vs/base/common/arrays.ts', 'vs/base/common/async.ts', 'vs/base/common/cache.ts', 'vs/base/common/cancellation.ts', 'vs/base/common/charCode.ts', 'vs/base/common/date.ts', 'vs/base/common/errors.ts', 'vs/base/common/event.ts', 'vs/base/common/functional.ts', 'vs/base/common/glob.ts', 'vs/base/common/htmlContent.ts', 'vs/base/common/iconLabels.ts', 'vs/base/common/iterator.ts', 'vs/base/common/lifecycle.ts', 'vs/base/common/linkedList.ts', 'vs/base/common/map.ts', 'vs/base/common/numbers.ts', 'vs/base/common/objects.ts', 'vs/base/common/resources.ts', 'vs/base/common/strings.ts', 'vs/base/common/ternarySearchTree.ts', 'vs/base/common/themables.ts', 'vs/base/common/uri.ts', 'vs/base/common/uuid.ts', 'vs/base/common/yaml.ts', 'vs/editor/common/core/ranges/offsetRange.ts', 'vs/editor/common/core/wordHelper.ts', 'vs/editor/common/model/prefixSumComputer.ts', 'vs/editor/common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.ts', 'vs/base/node/ports.ts', 'vs/platform/instantiation/common/instantiationService.ts', 'vs/editor/common/core/edits/lineEdit.ts', 'vs/editor/common/core/edits/lengthEdit.ts', 'vs/editor/common/core/edits/arrayEdit.ts', 'vs/editor/common/core/text/positionToOffset.ts', 'vs/editor/common/model/mirrorTextModel.ts', 'vs/workbench/api/common/extHostTypes/diagnostic.ts', 'vs/workbench/api/common/extHostTypes/location.ts', 'vs/workbench/api/common/extHostTypes/markdownString.ts', 'vs/workbench/api/common/extHostTypes/notebooks.ts', 'vs/workbench/api/common/extHostTypes/position.ts', 'vs/workbench/api/common/extHostTypes/range.ts', 'vs/workbench/api/common/extHostTypes/selection.ts', 'vs/workbench/api/common/extHostTypes/snippetString.ts', 'vs/workbench/api/common/extHostTypes/snippetTextEdit.ts', 'vs/workbench/api/common/extHostTypes/textEdit.ts', 'vs/workbench/api/common/extHostTypes/symbolInformation.ts', 'vs/workbench/api/common/extHostDocumentData.ts', 'vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts', 'vs/base/common/sseParser.ts', // SPECIAL IMPLICIT DEPENDENCIES 'typings/vscode-globals-nls.d.ts', 'typings/vscode-globals-product.d.ts', 'typings/base-common.d.ts', 'typings/crypto.d.ts', ]); } catch (error) { console.error(error); } })(); ================================================ FILE: script/setup/createVenv.mts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { execSync } from 'child_process'; import * as fs from 'fs'; const isWindows = process.platform === 'win32'; let python: string | undefined; function findPython(): void { if (python) { return; } // check if a venv already exists const pythonPath = isWindows ? '.venv\\Scripts\\python.exe' : '.venv/bin/python'; if (fs.existsSync(pythonPath)) { python = pythonPath; return; } // look for global python installations const pythonCommands = ['python3', 'python', 'py']; for (const pythonCommand of pythonCommands) { try { execSync(`${pythonCommand} --version`, { stdio: 'ignore' }); python = pythonCommand; return; } catch { continue; } } python = undefined; } function checkPythonVersion() { try { console.log(`Checking python: ${python} --version`); // Version must match `pyproject.toml` requirements execSync(`${python} -c "import sys;version=sys.version_info;print(version);assert (3,10) <= version < (3,13),'Python version must be >=3.10, < 3.13'"`, { encoding: 'utf8', stdio: 'inherit' }); } catch (error) { process.exit(1); } } let uv: string | undefined = undefined; function findUv(): void { const uvPath = isWindows ? '.venv\\Scripts\\uv.exe' : '.venv/bin/uv'; try { execSync(`${uvPath} --version`, { stdio: 'ignore' }); uv = uvPath; } catch { // ignore } try { // look for global `uv` execSync(`uv --version`, { stdio: 'ignore' }); uv = 'uv'; } catch { uv = undefined; } } function runCommand(command: string) { console.log(`Running command: ${command}`); execSync(command, { stdio: 'inherit' }); } function installRequirements(uvCommand: string) { runCommand(`${uvCommand} -r test/requirements.txt`); } function prepareVenv() { if (!fs.existsSync('test/requirements.txt')) { console.log('No requirements.txt found. Skipping virtual environment creation.'); return; } findPython(); findUv(); if (python === undefined && !uv) { console.error('No python cli found. Please install python and add it to your PATH.'); process.exit(1); } if (!fs.existsSync('.venv/pyvenv.cfg')) { console.log('Creating virtual environment...'); if (uv) { runCommand('uv venv --python 3.12 --seed .venv'); } else { checkPythonVersion(); runCommand(`${python} -m venv .venv`); } } const pythonPath = isWindows ? '.venv\\Scripts\\python.exe' : '.venv/bin/python'; if (!uv) { runCommand(`${pythonPath} -m pip install uv`); uv = isWindows ? '.venv\\Scripts\\uv.exe' : '.venv/bin/uv'; } const uvCommand = `${uv} pip install --python ${pythonPath}`; installRequirements(uvCommand); } prepareVenv(); ================================================ FILE: script/setup/getEnv.mts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { AzureCliCredential, ChainedTokenCredential, DeviceCodeCredential, InteractiveBrowserCredential, ManagedIdentityCredential, TokenCredential } from '@azure/identity'; import { SecretClient } from '@azure/keyvault-secrets'; import * as fs from 'fs'; import * as process from 'process'; const useColors = (process.stdout.isTTY && typeof process.stdout.hasColors === 'function' && process.stdout.hasColors()); function red(s: string) { return useColors ? `\x1b[31m${s}\x1b[0m` : s; } async function setupSecretClient(vaultUri: string) { const credentialOptions: TokenCredential[] = []; // Only add managed identity credential if the client ID is provided if (process.env.AZURE_CLIENT_ID) { credentialOptions.push(new ManagedIdentityCredential({ clientId: process.env.AZURE_CLIENT_ID })); } // Always add the Azure CLI as an option credentialOptions.push(new AzureCliCredential({ tenantId: "72f988bf-86f1-41af-91ab-2d7cd011db47" })); // Check if terminal is interactive, non-interactive environments can't use // InteractiveBrowserCredential and don't necessarily have access to a keychain // For SSH sessions into Azure VMs, keychain is not available, requires managed identity if (process.stdin.isTTY && !process.env.AZURE_CLIENT_ID && !process.env.CODESPACES) { credentialOptions.push(new InteractiveBrowserCredential({ tenantId: "72f988bf-86f1-41af-91ab-2d7cd011db47" })); } // Use DeviceCodeCredential in Codespaces if (process.env.CODESPACES) { const deviceCodeCredential = new DeviceCodeCredential({ tenantId: "72f988bf-86f1-41af-91ab-2d7cd011db47", userPromptCallback: (info) => { console.log("To authenticate, visit:", info.verificationUri); console.log("Enter the code:", info.userCode); } }); credentialOptions.push(deviceCodeCredential); } const credential = new ChainedTokenCredential(...credentialOptions); return new SecretClient(vaultUri, credential); } async function fetchSecret(secretClient: SecretClient, secretName: string): Promise { const secret = await secretClient.getSecret(secretName); return secret.value; } async function fetchSecrets(): Promise<{ [key: string]: string | undefined }> { const keyVaultClient = await setupSecretClient("https://copilot-automation.vault.azure.net/"); const secrets: { [key: string]: string | undefined } = {}; secrets["HMAC_SECRET"] = await fetchSecret(keyVaultClient, "hmac-secret"); if (!process.stdin.isTTY) { // only in automation secrets["GITHUB_OAUTH_TOKEN"] = await fetchSecret(keyVaultClient, "capi-oauth"); secrets["VSCODE_COPILOT_CHAT_TOKEN"] = await fetchSecret(keyVaultClient, "copilot-token"); secrets["GHCR_PAT"] = await fetchSecret(keyVaultClient, "ghcr-pat"); secrets["BLACKBIRD_EMBEDDINGS_KEY"] = await fetchSecret(keyVaultClient, "vsc-aoai-key"); secrets["BLACKBIRD_REDIS_CACHE_KEY"] = await fetchSecret(keyVaultClient, "blackbird-redis-cache-key"); try { secrets["ANTHROPIC_API_KEY"] = await fetchSecret(keyVaultClient, "anthropic-key"); secrets["DEEPSEEK_API_KEY"] = await fetchSecret(keyVaultClient, "deepseek-key"); } catch (error) { console.log(red(`Failed to fetch optional evaluation tokens. Skipping...`)); } } return secrets; } async function main() { const env = Object.entries(await fetchSecrets()); const raw = fs.existsSync('.env') ? fs.readFileSync('.env', 'utf8') : ''; const result = raw.split('\n') .filter(line => !env.some(([key]) => line.startsWith(`${key}=`))) .concat(env.map(([key, value]) => `${key}=${value}`)) .filter(line => line.trim() !== '') // Remove empty lines .join('\n'); fs.writeFileSync('.env', result); console.log('Wrote secrets to .env'); process.exit(0); } main().catch(error => { console.error(red(`Error when setting up .env file:\n${error}`)); }); ================================================ FILE: script/setup/getToken.mts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; import open from 'open'; const REQUEST1_URL = 'https://github.com/login/device/code'; const REQUEST2_URL = 'https://github.com/login/oauth/access_token'; // this is the VS Code OAuth app that the GitHub Authentication extension also uses const CLIENT_ID = '01ab8ac9400c4e429b23'; const keypress = async () => { if (process.stdin.isTTY) { process.stdin.setRawMode(true); } return new Promise(resolve => process.stdin.once('data', data => { const byteArray = [...data]; if (byteArray.length > 0 && byteArray[0] === 3) { console.log('^C'); process.exit(1); } if (process.stdin.isTTY) { process.stdin.setRawMode(false); } resolve(); }) ); }; async function main(): Promise { const requestOptions: RequestInit = { method: 'POST', body: JSON.stringify({ client_id: CLIENT_ID, // Needed for codesearch to access any private repos scope: 'repo', }), headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, }; const request1 = await fetch(REQUEST1_URL, requestOptions); const response1 = (await request1.json()) as any; console.log(`Copy this code: ${response1.user_code}`); console.log('Then press any key to launch the authorization page, paste the code in and approve the access.'); console.log(`It will take up to ${response1.interval} seconds after approval for the token to be retrieved.`); await keypress(); console.log(`Attempting to open ${response1.verification_uri}, if it doesn't open please manually navigate to the link and paste the code.`); const timeout = new Promise((resolve) => setTimeout(resolve, 5000)); await Promise.race([open(response1.verification_uri), timeout]); let expiresIn = response1.expires_in; let accessToken: undefined | string; while (expiresIn > 0) { const requestOptions: RequestInit = { method: 'POST', body: JSON.stringify({ client_id: CLIENT_ID, device_code: response1.device_code, grant_type: 'urn:ietf:params:oauth:grant-type:device_code', }), headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, }; const response2 = (await (await fetch(REQUEST2_URL, requestOptions)).json()) as any; expiresIn -= response1.interval; await new Promise(resolve => setTimeout(resolve, 1000 * response1.interval)); if (response2.access_token) { accessToken = response2.access_token; break; } } if (accessToken === undefined) { console.log('Timed out waiting for authorization'); process.exit(1); } else { const raw = fs.existsSync('.env') ? fs.readFileSync('.env', 'utf8') : ''; const result = raw.split('\n') .filter(line => !line.startsWith('GITHUB_OAUTH_TOKEN=')) .concat([`GITHUB_OAUTH_TOKEN=${accessToken}`]) .filter(line => line.trim() !== '') // Remove empty lines .join('\n'); fs.writeFileSync('.env', result); console.log('Wrote token to .env'); process.exit(0); } } if (!process.stdin.isTTY) { console.log('Not running in a TTY environment, skipping token generation.'); process.exit(0); } main(); ================================================ FILE: script/simulate.ps1 ================================================ #--------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. #--------------------------------------------------------------------------------------------- $ErrorActionPreference = "Stop" $ROOT = Split-Path -Parent (Split-Path -Parent (Resolve-Path $MyInvocation.MyCommand.Path)) $ELTRON = "$ROOT\\node_modules\\electron\\dist\\electron" Set-Location $ROOT & $ELTRON ./script/electron/simulationWorkbenchMain.js $args ================================================ FILE: script/simulate.sh ================================================ #!/usr/bin/env bash #--------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. #--------------------------------------------------------------------------------------------- set -e if [[ "$OSTYPE" == "darwin"* ]]; then realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } ROOT=$(dirname "$(dirname "$(realpath "$0")")") ELTRON="$ROOT/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron" else ROOT=$(dirname "$(dirname "$(readlink -f $0)")") ELTRON="$ROOT/node_modules/electron/dist/electron" fi cd "$ROOT" exec "$ELTRON" ./script/electron/simulationWorkbenchMain.js "$@" exit $? ================================================ FILE: script/test/scoredEditsReconciler.spec.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { expect, suite, test } from 'vitest'; import { resolveMergeConflict } from '../scoredEditsReconciler'; suite('can resolve merge conflicts', () => { test('1', () => { const fileContents = `{ "$web-editor.format-json": true, "$web-editor.default-url": "https://microsoft.github.io/vscode-workbench-recorder-viewer/?editRating", "edits": [ { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": null, "scoreCategory": "bad", "score": 0 }, { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": [ [ 1295, 1295, ");\\n\\t}\\n\\n\\tresetLastEditTime() {\\n\\t\\tthis._lastEditTime.set(undefined, undefined" ] ], "scoreCategory": "bad", "score": 0 }, { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": [ [ 1295, 1295, ");\\n\\t}\\n\\t\\n\\tpublic getLastEditTime(): number | undefined {\\n\\t\\treturn this._lastEditTime.get(" ] ], "scoreCategory": "bad", "score": 0 }, { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": [ [ 787, 864, "lastEditTime !== undefined && Date.now() - lastEditTime" ] ], "scoreCategory": "nextEdit", "score": 0 }, { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": [ [ 780, 893, "if (lastEditTime === undefined) {\\n\\t\\t\\treturn false;\\n\\t\\t}\\n\\t\\treturn Date.now() - lastEditTime < 5000" ] ], "scoreCategory": "nextEdit", "score": 0 }, { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": [ [ 780, 893, "if (lastEditTime === undefined) {\\n\\t\\t\\treturn false;\\n\\t\\t}\\n\\t\\treturn Date.now() - lastEditTime < 1000" ] ], "scoreCategory": "nextEdit", "score": 0 }, { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": [ [ 787, 893, "lastEditTime !== undefined && Date.now() - lastEditTime < 30 * 1000 /* " ] ], "scoreCategory": "bad", "score": 0 }, { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": [ [ 787, 893, "lastEditTime !== undefined && Date.now() - lastEditTime < 30 * 1000" ] ], "scoreCategory": "nextEdit", "score": 0 }, { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": [ [ <<<<<<< HEAD 893, 893, ";\\n\\t}\\n\\n\\tresetLastEditTime() {\\n\\t\\tthis._lastEditTime.set(undefined, undefined)" ] ], "scoreCategory": "bad", ======= 787, 894, "lastEditTime !== undefined && (Date.now() - lastEditTime) < 5000; // 5 seconds" ] ], "scoreCategory": "nextEdit", "score": 0 }, { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": [ [ 787, 894, "lastEditTime !== undefined && Date.now() - lastEditTime < 5000; // 5 seconds" ] ], "scoreCategory": "nextEdit", >>>>>>> a60bc6ab1 (nes: nearby: trim system message and run and score stests) "score": 0 } ], "scoringContext": { "kind": "recording", "recording": { "log": [ { "kind": "meta", "data": { "kind": "log-origin", "uuid": "a29a16dc-e6a3-41a7-9ebc-6c83958f00c9", "repoRootUri": "file:///users/ulugbekna/code/vscode-copilot", "opStart": 54006, "opEndEx": 54298 } }, { "kind": "documentEncountered", "id": 1, "time": 1733841300283, "relativePath": "../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts" }, { "kind": "setContent", "id": 1, "time": 1733841300283, "content": "/*---------------------------------------------------------------------------------------------\\n * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.\\n *--------------------------------------------------------------------------------------------*/\\n\\nimport { Disposable } from '../../../util/vs/base/common/lifecycle';\\nimport { mapObservableArrayCached, observableValue, runOnChange } from '../../../util/vs/base/common/observable';\\nimport { VSCodeWorkspace } from './vscodeWorkspace';\\n\\nexport class LastEditTimeTracker extends Disposable {\\n\\n\\tprivate readonly _lastEditTime = observableValue(this, undefined);\\n\\tpublic readonly lastEditTime = this._lastEditTime;\\n\\n\\tconstructor(\\n\\t\\tworkspace: VSCodeWorkspace,\\n\\t) {\\n\\t\\tsuper();\\n\\n\\t\\tmapObservableArrayCached(this, workspace.openDocuments, (doc, store) => {\\n\\t\\t\\tstore.add(runOnChange(doc.value, (_curState, _oldState, deltas) => {\\n\\t\\t\\t\\tif (deltas.length > 0 && deltas.some(edit => edit.edits.length > 0)) {\\n\\t\\t\\t\\t\\tthis._lastEditTime.set(Date.now(), undefined);\\n\\t\\t\\t\\t}\\n\\t\\t\\t}));\\n\\t\\t}).recomputeInitiallyAndOnChange(this._store);\\n\\t}\\n}\\n", "v": 2983 }, { "kind": "changed", "id": 1, "time": 1733841247985, "edit": [ [ 648, 648, "// " ] ], "v": 2986 }, { "kind": "changed", "id": 1, "time": 1733841249517, "edit": [ [ 701, 701, "\\n\\t\\n\\t" ] ], "v": 2998 }, { "kind": "changed", "id": 1, "time": 1733841250520, "edit": [ [ 702, 703, "" ] ], "v": 3002 }, { "kind": "changed", "id": 1, "time": 1733841250833, "edit": [ [ 704, 704, "get " ] ], "v": 3017 }, { "kind": "changed", "id": 1, "time": 1733841253100, "edit": [ [ 703, 708, "\\tget lastEditTime() {\\n\\t}" ] ], "v": 3029 }, { "kind": "changed", "id": 1, "time": 1733841254916, "edit": [ [ 708, 720, "" ] ], "v": 3035 }, { "kind": "changed", "id": 1, "time": 1733841260333, "edit": [], "v": 3050 }, { "kind": "changed", "id": 1, "time": 1733841262639, "edit": [ [ 708, 708, "hadEdits" ] ], "v": 3082 }, { "kind": "changed", "id": 1, "time": 1733841267155, "edit": [ [ 716, 716, "Recently" ] ], "v": 3122 }, { "kind": "changed", "id": 1, "time": 1733841269428, "edit": [ [ 728, 728, "\\n\\t\\t" ] ], "v": 3127 }, { "kind": "changed", "id": 1, "time": 1733841275244, "edit": [ [ 729, 731, "\\t\\treturn this._lastEditTime.get() !== undefined && Date.now() - this._lastEditTime.get() < 1000;" ] ], "v": 3131 }, { "kind": "changed", "id": 1, "time": 1733841278240, "edit": [ [ 820, 824, "30000" ] ], "v": 3167 }, { "kind": "changed", "id": 1, "time": 1733841286372, "edit": [ [ 820, 825, "30 * 1000 /* " ] ], "v": 3264 }, { "kind": "changed", "id": 1, "time": 1733841287540, "edit": [ [ 729, 834, "\\t\\treturn this._lastEditTime.get() !== undefined && Date.now() - this._lastEditTime.get() < 30 * 1000 /* 30 seconds */;" ] ], "v": 3268 }, { "kind": "changed", "id": 1, "time": 1733841293443, "edit": [ [ 811, 817, "." ] ], "v": 3308 }, { "kind": "changed", "id": 1, "time": 1733841294858, "edit": [ [ 812, 812, "get" ] ], "v": 3320 }, { "kind": "changed", "id": 1, "time": 1733841298602, "edit": [ [ 728, 728, "\\n\\t\\tconst " ] ], "v": 3350 }, { "kind": "changed", "id": 1, "time": 1733841300283, "edit": [ [ 729, 737, "\\t\\tconst lastEditTime = this._lastEditTime.get();" ] ], "v": 3354 } ], "nextUserEdit": { "edit": [ [ 787, 811, "lastEditTime" ], [ 842, 864, "lastEditTime" ] ], "relativePath": "../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "originalOpIdx": 54392 } } } }`; const resolvedFile = resolveMergeConflict(fileContents); expect(resolvedFile).toMatchInlineSnapshot(` "{ "$web-editor.format-json": true, "$web-editor.default-url": "https://microsoft.github.io/vscode-workbench-recorder-viewer/?editRating", "edits": [ { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": null, "scoreCategory": "bad", "score": 0 }, { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": [ [ 1295, 1295, ");\\n\\t}\\n\\n\\tresetLastEditTime() {\\n\\t\\tthis._lastEditTime.set(undefined, undefined" ] ], "scoreCategory": "bad", "score": 0 }, { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": [ [ 1295, 1295, ");\\n\\t}\\n\\t\\n\\tpublic getLastEditTime(): number | undefined {\\n\\t\\treturn this._lastEditTime.get(" ] ], "scoreCategory": "bad", "score": 0 }, { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": [ [ 787, 864, "lastEditTime !== undefined && Date.now() - lastEditTime" ] ], "scoreCategory": "nextEdit", "score": 0 }, { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": [ [ 780, 893, "if (lastEditTime === undefined) {\\n\\t\\t\\treturn false;\\n\\t\\t}\\n\\t\\treturn Date.now() - lastEditTime < 5000" ] ], "scoreCategory": "nextEdit", "score": 0 }, { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": [ [ 780, 893, "if (lastEditTime === undefined) {\\n\\t\\t\\treturn false;\\n\\t\\t}\\n\\t\\treturn Date.now() - lastEditTime < 1000" ] ], "scoreCategory": "nextEdit", "score": 0 }, { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": [ [ 787, 893, "lastEditTime !== undefined && Date.now() - lastEditTime < 30 * 1000 /* " ] ], "scoreCategory": "bad", "score": 0 }, { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": [ [ 787, 893, "lastEditTime !== undefined && Date.now() - lastEditTime < 30 * 1000" ] ], "scoreCategory": "nextEdit", "score": 0 }, { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": [ [ 893, 893, ";\\n\\t}\\n\\n\\tresetLastEditTime() {\\n\\t\\tthis._lastEditTime.set(undefined, undefined)" ] ], "scoreCategory": "bad", "score": 0 }, { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": [ [ 787, 894, "lastEditTime !== undefined && (Date.now() - lastEditTime) < 5000; // 5 seconds" ] ], "scoreCategory": "nextEdit", "score": 0 }, { "documentUri": "file:///users/ulugbekna/code/vscode-copilot/../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "edit": [ [ 787, 894, "lastEditTime !== undefined && Date.now() - lastEditTime < 5000; // 5 seconds" ] ], "scoreCategory": "nextEdit", "score": 0 } ], "scoringContext": { "kind": "recording", "recording": { "log": [ { "kind": "meta", "data": { "kind": "log-origin", "uuid": "a29a16dc-e6a3-41a7-9ebc-6c83958f00c9", "repoRootUri": "file:///users/ulugbekna/code/vscode-copilot", "opStart": 54006, "opEndEx": 54298 } }, { "kind": "documentEncountered", "id": 1, "time": 1733841300283, "relativePath": "../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts" }, { "kind": "setContent", "id": 1, "time": 1733841300283, "content": "/*---------------------------------------------------------------------------------------------\\n * Copyright (c) Microsoft Corporation and GitHub. All rights reserved.\\n *--------------------------------------------------------------------------------------------*/\\n\\nimport { Disposable } from '../../../util/vs/base/common/lifecycle';\\nimport { mapObservableArrayCached, observableValue, runOnChange } from '../../../util/vs/base/common/observable';\\nimport { VSCodeWorkspace } from './vscodeWorkspace';\\n\\nexport class LastEditTimeTracker extends Disposable {\\n\\n\\tprivate readonly _lastEditTime = observableValue(this, undefined);\\n\\tpublic readonly lastEditTime = this._lastEditTime;\\n\\n\\tconstructor(\\n\\t\\tworkspace: VSCodeWorkspace,\\n\\t) {\\n\\t\\tsuper();\\n\\n\\t\\tmapObservableArrayCached(this, workspace.openDocuments, (doc, store) => {\\n\\t\\t\\tstore.add(runOnChange(doc.value, (_curState, _oldState, deltas) => {\\n\\t\\t\\t\\tif (deltas.length > 0 && deltas.some(edit => edit.edits.length > 0)) {\\n\\t\\t\\t\\t\\tthis._lastEditTime.set(Date.now(), undefined);\\n\\t\\t\\t\\t}\\n\\t\\t\\t}));\\n\\t\\t}).recomputeInitiallyAndOnChange(this._store);\\n\\t}\\n}\\n", "v": 2983 }, { "kind": "changed", "id": 1, "time": 1733841247985, "edit": [ [ 648, 648, "// " ] ], "v": 2986 }, { "kind": "changed", "id": 1, "time": 1733841249517, "edit": [ [ 701, 701, "\\n\\t\\n\\t" ] ], "v": 2998 }, { "kind": "changed", "id": 1, "time": 1733841250520, "edit": [ [ 702, 703, "" ] ], "v": 3002 }, { "kind": "changed", "id": 1, "time": 1733841250833, "edit": [ [ 704, 704, "get " ] ], "v": 3017 }, { "kind": "changed", "id": 1, "time": 1733841253100, "edit": [ [ 703, 708, "\\tget lastEditTime() {\\n\\t}" ] ], "v": 3029 }, { "kind": "changed", "id": 1, "time": 1733841254916, "edit": [ [ 708, 720, "" ] ], "v": 3035 }, { "kind": "changed", "id": 1, "time": 1733841260333, "edit": [], "v": 3050 }, { "kind": "changed", "id": 1, "time": 1733841262639, "edit": [ [ 708, 708, "hadEdits" ] ], "v": 3082 }, { "kind": "changed", "id": 1, "time": 1733841267155, "edit": [ [ 716, 716, "Recently" ] ], "v": 3122 }, { "kind": "changed", "id": 1, "time": 1733841269428, "edit": [ [ 728, 728, "\\n\\t\\t" ] ], "v": 3127 }, { "kind": "changed", "id": 1, "time": 1733841275244, "edit": [ [ 729, 731, "\\t\\treturn this._lastEditTime.get() !== undefined && Date.now() - this._lastEditTime.get() < 1000;" ] ], "v": 3131 }, { "kind": "changed", "id": 1, "time": 1733841278240, "edit": [ [ 820, 824, "30000" ] ], "v": 3167 }, { "kind": "changed", "id": 1, "time": 1733841286372, "edit": [ [ 820, 825, "30 * 1000 /* " ] ], "v": 3264 }, { "kind": "changed", "id": 1, "time": 1733841287540, "edit": [ [ 729, 834, "\\t\\treturn this._lastEditTime.get() !== undefined && Date.now() - this._lastEditTime.get() < 30 * 1000 /* 30 seconds */;" ] ], "v": 3268 }, { "kind": "changed", "id": 1, "time": 1733841293443, "edit": [ [ 811, 817, "." ] ], "v": 3308 }, { "kind": "changed", "id": 1, "time": 1733841294858, "edit": [ [ 812, 812, "get" ] ], "v": 3320 }, { "kind": "changed", "id": 1, "time": 1733841298602, "edit": [ [ 728, 728, "\\n\\t\\tconst " ] ], "v": 3350 }, { "kind": "changed", "id": 1, "time": 1733841300283, "edit": [ [ 729, 737, "\\t\\tconst lastEditTime = this._lastEditTime.get();" ] ], "v": 3354 } ], "nextUserEdit": { "edit": [ [ 787, 811, "lastEditTime" ], [ 842, 864, "lastEditTime" ] ], "relativePath": "../../../../Users/ulugbekna/code/vscode-copilot/src/extension/inlineEdits/vscode-node/lastEditTimeTracker.ts", "originalOpIdx": 54392 } } } }" `); }); }); ================================================ FILE: script/testGeneration/editFromPatchTests.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { promises as fs } from 'fs'; import minimist from 'minimist'; import * as path from 'path'; const fixturesRootsFolder = path.join(__dirname, '../../src/extension/test/node/fixtures/patch'); const simulationsRootFolder = path.join(__dirname, '../../.simulation'); async function main(simulationFolder: string | undefined, all: boolean | undefined, annotationFilter: string = 'invalid patch'): Promise { if (!simulationFolder || !await checkExists(simulationsRootFolder)) { const lastRunName = await findLastRun(simulationsRootFolder); if (!lastRunName) { console.log(`No run found in ${simulationsRootFolder}`); return; } simulationFolder = path.join(simulationsRootFolder, lastRunName); } const outputFixturesFolder = path.join(fixturesRootsFolder, path.basename(simulationFolder)); console.log(`Looking for stest results in ${simulationFolder}`); const entries = await fs.readdir(simulationFolder); for (const entry of entries) { for (let testRun = 0; testRun < 10; testRun++) { const simTextPath = path.join(simulationFolder, entry, `0${testRun}-inline-simulator.txt`); if (!await checkExists(simTextPath)) { break; } const simText = JSON.parse(await fs.readFile(simTextPath, 'utf8')) as any[]; if (!all) { const isInvalidPatch = (() => { for (let i = 0; i < simText.length; i++) { const data = simText[i]; if (data.kind === 'interaction' && Array.isArray(data.annotations)) { for (const annotation of data.annotations) { if (annotation.label === annotationFilter) { return true; } } } } return undefined; })(); if (!isInvalidPatch) { continue; } } const simRequest = JSON.parse(await fs.readFile(path.join(simulationFolder, entry, `0${testRun}-sim-requests.txt`), 'utf8')) as any[]; const originalFileEntry = (() => { for (let i = 0; i < simText.length; i++) { const data = simText[i]; if (data.kind === 'initial') { return data.file; } } return undefined; })(); if (!originalFileEntry) { console.log('No original file path found'); break; } const original = await fs.readFile(path.join(simulationFolder, originalFileEntry.relativeDiskPath), 'utf8'); const modifedFilePath = (() => { for (let i = 0; i < simText.length; i++) { const data = simText[i]; if (data.kind === 'interaction' && Array.isArray(data.changedFiles)) { for (const changedFile of data.changedFiles) { if (changedFile.workspacePath === originalFileEntry.workspacePath) { return changedFile.relativeDiskPath; } } } } return undefined; })(); if (!modifedFilePath) { console.log('No modified file path found'); break; } const modified = await fs.readFile(path.join(simulationFolder, modifedFilePath), 'utf8'); const response = simRequest[0].response.value.join(''); const name = `${entry}0${testRun}`; console.log(`Writing fixtures for ${name} at ${outputFixturesFolder}`); await fs.mkdir(outputFixturesFolder, { recursive: true }); await fs.writeFile(path.join(outputFixturesFolder, `${name}.original.txt`), original); await fs.writeFile(path.join(outputFixturesFolder, `${name}.expected.txt`), modified); await fs.writeFile(path.join(outputFixturesFolder, `${name}.patch.txt`), response); } } } async function findLastRun(simulationsRootFolder: string): Promise { const entries = await fs.readdir(simulationsRootFolder); return entries.filter(entry => entry.match(/^out-\d{8}-\d{6}$/)).sort().pop(); } async function checkExists(filePath: string): Promise { try { await fs.stat(filePath); return true; } catch (error) { return false; } } if (require.main === module) { const parsedArgs = minimist(process.argv); if (parsedArgs.help) { console.log('Usage: npx tsx editFromPatchTests.ts --simulation-folder --all --annotation '); process.exit(0); } main(parsedArgs['simulation-folder'], parsedArgs['all'], parsedArgs['annotation']).catch(err => { console.error(err); process.exit(1); }); } ================================================ FILE: script/tsconfig.json ================================================ { "compilerOptions": { "module": "Node16", "moduleResolution": "Node16", "noEmit": true, "target": "ES2022", "lib": ["ES2022" ], "strict": true }, "include": [ ".", ], } ================================================ FILE: src/extension/agentDebug/common/toolResultRenderer.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { createServiceIdentifier } from '../../../util/common/services'; export const IToolResultContentRenderer = createServiceIdentifier('IToolResultContentRenderer'); /** * Renders tool result content parts into human-readable strings. * Injected from the vscode-node layer to avoid layering violations * (the rendering depends on @vscode/prompt-tsx which lives in vscode-node). */ export interface IToolResultContentRenderer { readonly _serviceBrand: undefined; /** * Extracts a text representation from the content parts of a tool result. * Handles LanguageModelTextPart, LanguageModelPromptTsxPart, and LanguageModelDataPart. * Uses lightweight string conversion to avoid expensive rendering on the hot path. */ renderToolResultContent(content: Iterable): string[]; } ================================================ FILE: src/extension/agentDebug/vscode-node/toolResultContentRenderer.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { LanguageModelDataPart, LanguageModelPromptTsxPart, LanguageModelTextPart } from '../../../vscodeTypes'; import { renderDataPartToString } from '../../prompt/vscode-node/requestLoggerToolResult'; import { IToolResultContentRenderer } from '../common/toolResultRenderer'; export class ToolResultContentRenderer implements IToolResultContentRenderer { readonly _serviceBrand: undefined; renderToolResultContent(content: Iterable): string[] { const parts: string[] = []; for (const part of content) { if (part instanceof LanguageModelTextPart) { parts.push(part.value); } else if (part instanceof LanguageModelPromptTsxPart) { // Use lightweight JSON serialization instead of expensive renderPrompt(). // This runs on every tool call, so avoid async TSX rendering overhead. try { parts.push(JSON.stringify(part.value, null, 2)); } catch { parts.push('[PromptTsxPart]'); } } else if (part instanceof LanguageModelDataPart) { parts.push(renderDataPartToString(part)); } } return parts; } } ================================================ FILE: src/extension/agents/node/adapters/anthropicAdapter.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import Anthropic from '@anthropic-ai/sdk'; import * as http from 'http'; import type { OpenAiFunctionTool } from '../../../../platform/networking/common/fetch'; import { IMakeChatRequestOptions } from '../../../../platform/networking/common/networking'; import { APIUsage } from '../../../../platform/networking/common/openai'; import { coalesce } from '../../../../util/vs/base/common/arrays'; import { anthropicMessagesToRawMessages } from '../../../byok/common/anthropicMessageConverter'; import { IAgentStreamBlock, IParsedRequest, IProtocolAdapter, IProtocolAdapterFactory, IStreamEventData, IStreamingContext } from './types'; export class AnthropicAdapterFactory implements IProtocolAdapterFactory { createAdapter(): IProtocolAdapter { return new AnthropicAdapter(); } } class AnthropicAdapter implements IProtocolAdapter { readonly name = 'anthropic'; // Per-request state private currentBlockIndex = 0; private hasTextBlock = false; private hadToolCalls = false; parseRequest(body: string): IParsedRequest { const requestBody: Anthropic.MessageStreamParams = JSON.parse(body); // Build a single system text block from "system" if provided let systemText = ''; if (typeof requestBody.system === 'string') { systemText = requestBody.system; } else if (Array.isArray(requestBody.system) && requestBody.system.length > 0) { systemText = requestBody.system.map(s => s.text).join('\n'); } const type = systemText.includes('You are a helpful AI assistant tasked with summarizing conversations') ? 'summary' : undefined; // Convert Anthropic messages to Raw (TSX) messages const rawMessages = anthropicMessagesToRawMessages(requestBody.messages, { type: 'text', text: systemText }); const options: IMakeChatRequestOptions['requestOptions'] = { temperature: requestBody.temperature, }; if (requestBody.tools && requestBody.tools.length > 0) { // Map Anthropic tools to VS Code chat tools. Provide a no-op invoke since this server doesn't run tools. const tools = coalesce(requestBody.tools.map(tool => { if ('input_schema' in tool) { const chatTool: OpenAiFunctionTool = { type: 'function', function: { name: tool.name, description: tool.description || '', parameters: tool.input_schema || {}, } }; return chatTool; } return undefined; })); if (tools.length) { options.tools = tools; } } return { model: requestBody.model, messages: rawMessages, options, type }; } formatStreamResponse( streamData: IAgentStreamBlock, context: IStreamingContext ): IStreamEventData[] { const events: IStreamEventData[] = []; if (streamData.type === 'text') { if (!this.hasTextBlock) { // Send content_block_start for text const contentBlockStart: Anthropic.RawContentBlockStartEvent = { type: 'content_block_start', index: this.currentBlockIndex, content_block: { type: 'text', text: '', citations: null } }; events.push({ event: contentBlockStart.type, data: this.formatEventData(contentBlockStart) }); this.hasTextBlock = true; } // Send content_block_delta for text const contentDelta: Anthropic.RawContentBlockDeltaEvent = { type: 'content_block_delta', index: this.currentBlockIndex, delta: { type: 'text_delta', text: streamData.content } }; events.push({ event: contentDelta.type, data: this.formatEventData(contentDelta) }); } else if (streamData.type === 'tool_call') { // End current text block if it exists if (this.hasTextBlock) { const contentBlockStop: Anthropic.RawContentBlockStopEvent = { type: 'content_block_stop', index: this.currentBlockIndex }; events.push({ event: contentBlockStop.type, data: this.formatEventData(contentBlockStop) }); this.currentBlockIndex++; this.hasTextBlock = false; } this.hadToolCalls = true; // Send tool use block const toolBlockStart: Anthropic.RawContentBlockStartEvent = { type: 'content_block_start', index: this.currentBlockIndex, content_block: { type: 'tool_use', id: streamData.callId, name: streamData.name, input: {}, caller: { type: 'direct' }, } }; events.push({ event: toolBlockStart.type, data: this.formatEventData(toolBlockStart) }); // Send tool use content const toolBlockContent: Anthropic.RawContentBlockDeltaEvent = { type: 'content_block_delta', index: this.currentBlockIndex, delta: { type: 'input_json_delta', partial_json: JSON.stringify(streamData.input || {}) } }; events.push({ event: toolBlockContent.type, data: this.formatEventData(toolBlockContent) }); const toolBlockStop: Anthropic.RawContentBlockStopEvent = { type: 'content_block_stop', index: this.currentBlockIndex }; events.push({ event: toolBlockStop.type, data: this.formatEventData(toolBlockStop) }); this.currentBlockIndex++; } return events; } generateFinalEvents(context: IStreamingContext, usage?: APIUsage): IStreamEventData[] { const events: IStreamEventData[] = []; // Send final events if (this.hasTextBlock) { const contentBlockStop: Anthropic.RawContentBlockStopEvent = { type: 'content_block_stop', index: this.currentBlockIndex }; events.push({ event: contentBlockStop.type, data: this.formatEventData(contentBlockStop) }); } // Adjust token usage to make the agent think it has a 200k context window // when the real one is smaller const adjustedUsage = this.adjustTokenUsageForContextWindow(context, usage); const messageDelta: Anthropic.RawMessageDeltaEvent = { type: 'message_delta', delta: { stop_reason: this.hadToolCalls ? 'tool_use' : 'end_turn', stop_sequence: null, container: null }, usage: { output_tokens: adjustedUsage.completion_tokens, cache_creation_input_tokens: 0, cache_read_input_tokens: 0, input_tokens: adjustedUsage.prompt_tokens, server_tool_use: null } }; events.push({ event: messageDelta.type, data: this.formatEventData(messageDelta) }); const messageStop: Anthropic.RawMessageStopEvent = { type: 'message_stop' }; events.push({ event: messageStop.type, data: this.formatEventData(messageStop) }); return events; } private adjustTokenUsageForContextWindow(context: IStreamingContext, usage?: APIUsage): APIUsage { // If we don't have usage, return defaults if (!usage) { return { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }; } // If we don't have endpoint info, return the unadjusted usage if (context.endpoint.modelId === 'gpt-4o-mini') { return usage; } const realContextLimit = context.endpoint.modelMaxPromptTokens; const agentAssumedContextLimit = 200000; // The agent thinks it has 200k tokens // Calculate scaling factor to make the agent think it has a larger context window // When the real usage approaches the real limit, the adjusted usage should approach the assumed limit const scalingFactor = agentAssumedContextLimit / realContextLimit; const adjustedPromptTokens = Math.floor(usage.prompt_tokens * scalingFactor); const adjustedCompletionTokens = Math.floor(usage.completion_tokens * scalingFactor); const adjustedTotalTokens = adjustedPromptTokens + adjustedCompletionTokens; return { ...usage, prompt_tokens: adjustedPromptTokens, completion_tokens: adjustedCompletionTokens, total_tokens: adjustedTotalTokens, }; } generateInitialEvents(context: IStreamingContext): IStreamEventData[] { // Use adjusted token usage for initial events to be consistent // For initial events, we don't have real usage yet, so we'll use defaults const adjustedUsage = this.adjustTokenUsageForContextWindow(context, undefined); // Send message_start event const messageStart: Anthropic.RawMessageStartEvent = { type: 'message_start', message: { id: context.requestId, type: 'message', role: 'assistant', model: context.endpoint.modelId, content: [], container: null, stop_reason: null, stop_sequence: null, usage: { input_tokens: adjustedUsage.prompt_tokens, cache_creation_input_tokens: 0, cache_read_input_tokens: 0, output_tokens: 1, service_tier: null, server_tool_use: null, cache_creation: null, } as Anthropic.Usage } }; return [{ event: messageStart.type, data: this.formatEventData(messageStart) }]; } getContentType(): string { return 'text/event-stream'; } extractAuthKey(headers: http.IncomingHttpHeaders): string | undefined { return headers['x-api-key'] as string | undefined; } private formatEventData(data: unknown): string { return JSON.stringify(data).replace(/\n/g, '\\n'); } } ================================================ FILE: src/extension/agents/node/adapters/openaiAdapterForSTests.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Raw } from '@vscode/prompt-tsx'; import { ChatCompletionContentPartKind } from '@vscode/prompt-tsx/dist/base/output/rawTypes'; import * as http from 'http'; import { ChatCompletionChunk, ChatCompletionCreateParamsBase, ChatCompletionMessageParam } from 'openai/resources/chat/completions.js'; import type { OpenAiFunctionTool } from '../../../../platform/networking/common/fetch'; import { IMakeChatRequestOptions } from '../../../../platform/networking/common/networking'; import { APIUsage } from '../../../../platform/networking/common/openai'; import { coalesce } from '../../../../util/vs/base/common/arrays'; import { IAgentStreamBlock, IParsedRequest, IProtocolAdapter, IProtocolAdapterFactory, IStreamEventData, IStreamingContext } from './types'; export class OpenAIAdapterFactoryForSTests implements IProtocolAdapterFactory { private readonly requestHooks: ((body: string) => string)[] = []; private readonly responseHooks: ((body: string) => string)[] = []; createAdapter(): IProtocolAdapter { return new OpenAIAdapterForSTests(this.requestHooks, this.responseHooks); } public addHooks(requestHook?: (body: string) => string, responseHook?: (body: string) => string): void { if (requestHook) { this.requestHooks.push(requestHook); } if (responseHook) { this.responseHooks.push(responseHook); } } } class OpenAIAdapterForSTests implements IProtocolAdapter { readonly name = 'openai'; // Per-request state private currentBlockIndex = 0; private hasTextBlock = false; private hadToolCalls = false; constructor(private readonly requestHooks: ((body: string) => string)[], private readonly responseHooks: ((body: string) => string)[] = []) { // No-op for test adapter } parseRequest(body: string): IParsedRequest { body = this.requestHooks.reduce((b, hook) => hook(b), body); const requestBody: ChatCompletionCreateParamsBase = JSON.parse(body); // Extract model information const model = requestBody.model; // Convert messages format if needed const runHooks = (msg: string) => { return this.requestHooks.reduce((b, hook) => hook(b), msg); }; const messages = responseApiInputToRawMessages(requestBody.messages); messages.forEach(msg => { msg.content.forEach(part => { switch (part.type) { case ChatCompletionContentPartKind.Image: { part.imageUrl.url = runHooks(part.imageUrl.url); break; } case ChatCompletionContentPartKind.Opaque: { if (typeof part.value === 'string') { part.value = runHooks(part.value); } break; } case ChatCompletionContentPartKind.Text: { part.text = runHooks(part.text); break; } } }); }); const options: IMakeChatRequestOptions['requestOptions'] = { temperature: (requestBody.temperature ?? undefined), max_tokens: (requestBody.max_tokens ?? requestBody.max_completion_tokens) ?? undefined, }; if (requestBody.tools && Array.isArray(requestBody.tools) && requestBody.tools.length > 0) { // Map OpenAI tools to VS Code chat tools const tools = coalesce(requestBody.tools.map((tool) => { if (tool.type === 'function' && tool.function) { const chatTool: OpenAiFunctionTool = { type: 'function', function: { name: tool.function.name, description: tool.function.description || '', parameters: tool.function.parameters || {}, } }; return chatTool; } return undefined; })); if (tools.length) { options.tools = tools; } } return { model, messages, options }; } private readonly textMessages = new Map(); private collectTextContent(context: IStreamingContext, content: string): void { const existing = this.textMessages.get(context.requestId) || ''; this.textMessages.set(context.requestId, existing + content); } private getCollectedTextContent(context: IStreamingContext): IStreamEventData | undefined { let content = this.textMessages.get(context.requestId); if (typeof content !== 'string') { return undefined; } this.textMessages.delete(context.requestId); content = this.responseHooks.reduce((b, hook) => hook(b), content); // Send text delta events const event = { id: context.requestId, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: context.endpoint.modelId, choices: [{ index: this.currentBlockIndex, delta: { content, role: 'assistant' }, finish_reason: null }] } satisfies ChatCompletionChunk; return { event: 'message', data: this.formatEventData(event) }; } formatStreamResponse( streamData: IAgentStreamBlock, context: IStreamingContext ): IStreamEventData[] { const events: IStreamEventData[] = []; if (streamData.type === 'text') { if (!this.hasTextBlock) { this.hasTextBlock = true; } // Collect all of the strings, as there could be references to file paths. // At the end of the stream, we will send a single event with the full text & have file paths replaced. this.collectTextContent(context, streamData.content); } else if (streamData.type === 'tool_call') { // End current text block if it exists if (this.hasTextBlock) { const event = this.getCollectedTextContent(context); if (event) { events.push(event); } this.currentBlockIndex++; this.hasTextBlock = false; } this.hadToolCalls = true; // Arguments can contain file paths. const toolArguments = this.responseHooks.reduce((b, hook) => hook(b), JSON.stringify(streamData.input || {})); // Send tool call events const toolCallDelta: ChatCompletionChunk = { id: context.requestId, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: context.endpoint.modelId, choices: [{ index: this.currentBlockIndex, delta: { tool_calls: [{ index: this.currentBlockIndex, id: streamData.callId, type: 'function', function: { name: streamData.name, arguments: toolArguments } }] }, finish_reason: null }] }; events.push({ event: 'message', data: this.formatEventData(toolCallDelta) }); this.currentBlockIndex++; } return events; } generateFinalEvents(context: IStreamingContext, usage?: APIUsage): IStreamEventData[] { const events: IStreamEventData[] = []; const event = this.getCollectedTextContent(context); if (event) { events.push(event); } // Send final completion event with usage information const finalCompletion = { id: context.requestId, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: context.endpoint.modelId, choices: [{ index: 0, delta: { content: null }, finish_reason: this.hadToolCalls ? 'tool_calls' : 'stop' }], usage: usage ? { prompt_tokens: usage.prompt_tokens, completion_tokens: usage.completion_tokens, total_tokens: usage.total_tokens } : { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 } } satisfies ChatCompletionChunk; events.push({ event: 'message', data: this.formatEventData(finalCompletion) }); return events; } generateInitialEvents(context: IStreamingContext): IStreamEventData[] { // OpenAI doesn't typically send initial events, but we can send an empty one if needed return []; } getContentType(): string { return 'text/event-stream'; } extractAuthKey(headers: http.IncomingHttpHeaders): string | undefined { const authHeader = headers.authorization; const bearerSpace = 'Bearer '; return authHeader?.startsWith(bearerSpace) ? authHeader.substring(bearerSpace.length) : undefined; } private formatEventData(data: unknown): string { return JSON.stringify(data).replace(/\n/g, '\\n'); } } function responseApiInputToRawMessages(messages: ChatCompletionMessageParam[]): Raw.ChatMessage[] { const raw: Raw.ChatMessage[] = []; // Helper to push or merge consecutive messages of same role const pushOrMerge = (msg: Raw.ChatMessage) => { const last = raw[raw.length - 1]; if (last && last.role === msg.role && last.role !== Raw.ChatRole.Tool) { // Merge content arrays last.content.push(...msg.content); // Merge tool calls if assistant if (last.role === Raw.ChatRole.Assistant && msg.role === Raw.ChatRole.Assistant && msg.toolCalls) { const l = last as Raw.AssistantChatMessage; l.toolCalls = [...(l.toolCalls || []), ...((msg as Raw.AssistantChatMessage).toolCalls || [])]; } } else { raw.push(msg); } }; messages.forEach(m => { // Collect content parts const contentParts: Raw.ChatCompletionContentPart[] = []; // OpenAI message content can be string or ChatCompletionContentPart[] (Array.isArray(m.content) ? m.content : []).forEach(part => { switch (part.type) { case 'text': { contentParts.push({ type: Raw.ChatCompletionContentPartKind.Text, text: part.text }); break; } case 'image_url': { contentParts.push({ imageUrl: { url: part.image_url.url, detail: part.image_url.detail as unknown as ('low' | 'high' | undefined) }, type: ChatCompletionContentPartKind.Image }); break; } case 'file': { contentParts.push({ type: ChatCompletionContentPartKind.Opaque, value: `[File Input - Filename: ${part.file.filename}]` }); break; } case 'refusal': { // Refusal parts contain a 'refusal' field; access defensively contentParts.push({ type: Raw.ChatCompletionContentPartKind.Text, text: `[Refusal: ${part.refusal || ''}]` }); break; } case 'input_audio': default: { // Unknown part } } }); if (typeof m.content === 'string') { contentParts.push({ type: Raw.ChatCompletionContentPartKind.Text, text: m.content }); } switch (m.role) { case 'user': { pushOrMerge({ role: Raw.ChatRole.User, content: contentParts }); return; } case 'tool': { // contentParts.splice(0, contentParts.length); raw.push({ role: Raw.ChatRole.Tool, content: contentParts, toolCallId: m.tool_call_id || '' }); return; } case 'assistant': { const toolCalls: Raw.ChatMessageToolCall[] = (m.tool_calls || []).map(tc => { try { if (tc.type === 'function') { return { id: tc.id || tc.function.name || 'tool_call', type: 'function', function: { name: tc.function.name || 'unknown_function', arguments: typeof tc.function.arguments === 'string' ? tc.function.arguments : JSON.stringify(tc.function.arguments ?? {}) } } satisfies Raw.ChatMessageToolCall; } } catch { } // Fallback minimal tool call return { id: 'tool_call', type: 'function', function: { name: 'unknown_function', arguments: '{}' } } satisfies Raw.ChatMessageToolCall; }); const message: Raw.AssistantChatMessage = { role: Raw.ChatRole.Assistant, content: contentParts }; if (toolCalls.length) { message.toolCalls = toolCalls; } pushOrMerge(message); return; } case 'system': case 'developer': { // System (and any unexpected) messages pushOrMerge({ role: Raw.ChatRole.System, content: contentParts, name: m.name }); return; } default: { return; } } }); return raw; } ================================================ FILE: src/extension/agents/node/adapters/types.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Raw } from '@vscode/prompt-tsx'; import * as http from 'http'; import type { IMakeChatRequestOptions } from '../../../../platform/networking/common/networking'; import { APIUsage } from '../../../../platform/networking/common/openai'; export interface IParsedRequest { readonly model?: string; readonly messages: readonly Raw.ChatMessage[]; readonly options?: IMakeChatRequestOptions['requestOptions']; readonly type?: string; } export interface IStreamEventData { readonly event: string; readonly data: string; } export interface IAgentTextBlock { readonly type: 'text'; readonly content: string; } export interface IAgentToolCallBlock { readonly type: 'tool_call'; readonly callId: string; readonly name: string; readonly input: object; } export type IAgentStreamBlock = IAgentTextBlock | IAgentToolCallBlock; export interface IProtocolAdapter { /** * The name of this protocol adapter */ readonly name: string; /** * Parse the incoming request body and convert to VS Code format */ parseRequest(body: string): IParsedRequest; /** * Convert raw streaming data to protocol-specific events */ formatStreamResponse( streamData: IAgentStreamBlock, context: IStreamingContext ): readonly IStreamEventData[]; /** * Generate the final events to close the stream */ generateFinalEvents(context: IStreamingContext, usage?: APIUsage): readonly IStreamEventData[]; /** * Generate initial events to start the stream (optional, protocol-specific) */ generateInitialEvents?(context: IStreamingContext): readonly IStreamEventData[]; /** * Get the content type for responses */ getContentType(): string; /** * Extract the authentication key/nonce from request headers */ extractAuthKey(headers: http.IncomingHttpHeaders): string | undefined; } export interface IProtocolAdapterFactory { /** * Create a new adapter instance for a request */ createAdapter(): IProtocolAdapter; } export interface IStreamingContext { readonly requestId: string; readonly endpoint: { readonly modelId: string; readonly modelMaxPromptTokens: number; }; } ================================================ FILE: src/extension/agents/node/langModelServer.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Raw } from '@vscode/prompt-tsx'; import * as http from 'http'; import { ChatFetchResponseType, ChatLocation } from '../../../platform/chat/common/commonTypes'; import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider'; import { ILogService } from '../../../platform/log/common/logService'; import { IChatEndpoint } from '../../../platform/networking/common/networking'; import { APIUsage } from '../../../platform/networking/common/openai'; import { createServiceIdentifier } from '../../../util/common/services'; import { CancellationTokenSource } from '../../../util/vs/base/common/cancellation'; import { generateUuid } from '../../../util/vs/base/common/uuid'; import { LanguageModelError } from '../../../vscodeTypes'; import { AnthropicAdapterFactory } from './adapters/anthropicAdapter'; import { IAgentStreamBlock, IProtocolAdapter, IProtocolAdapterFactory, IStreamingContext } from './adapters/types'; export interface ILanguageModelServerConfig { readonly port: number; readonly nonce: string; } export const ILanguageModelServer = createServiceIdentifier('ILanguageModelServer'); export interface ILanguageModelServer { readonly _serviceBrand: undefined; start(): Promise; stop(): void; getConfig(): ILanguageModelServerConfig; } export class LanguageModelServer implements ILanguageModelServer { declare _serviceBrand: undefined; private server: http.Server; protected config: ILanguageModelServerConfig; protected adapterFactories: Map; protected readonly requestHandlers = new Map Promise }>(); constructor( @ILogService private readonly logService: ILogService, @IEndpointProvider protected readonly endpointProvider: IEndpointProvider ) { this.config = { port: 0, // Will be set to random available port nonce: 'vscode-lm-' + generateUuid() }; this.adapterFactories = new Map(); this.adapterFactories.set('/v1/messages', new AnthropicAdapterFactory()); this.server = this.createServer(); } private createServer(): http.Server { return http.createServer(async (req, res) => { this.logService.trace(`Received request: ${req.method} ${req.url}`); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } const handler = this.requestHandlers.get(req.url || ''); if (handler && handler.method === req.method) { await handler.handler(req, res); return; } if (req.method === 'POST') { const adapterFactory = this.getAdapterFactoryForPath(req.url || ''); if (adapterFactory) { try { // Create new adapter instance for this request const adapter = adapterFactory.createAdapter(); const body = await this.readRequestBody(req); // Verify nonce for authentication const authKey = adapter.extractAuthKey(req.headers); if (authKey !== this.config.nonce) { this.logService.trace(`[LanguageModelServer] Invalid auth key`); res.writeHead(401, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Invalid authentication' })); return; } await this.handleChatRequest(adapter, body, res); } catch (error) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Internal server error', details: error instanceof Error ? error.message : String(error) })); } return; } } if (req.method === 'GET' && req.url === '/') { res.writeHead(200); res.end('Hello from LanguageModelServer'); return; } if (req.method === 'GET' && req.url === '/models') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ data: [] })); return; } res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Not found' })); }); } private parseUrlPathname(url: string): string { try { const parsedUrl = new URL(url, 'http://localhost'); return parsedUrl.pathname; } catch { return url.split('?')[0]; } } private getAdapterFactoryForPath(url: string): IProtocolAdapterFactory | undefined { const pathname = this.parseUrlPathname(url); return this.adapterFactories.get(pathname); } private async readRequestBody(req: http.IncomingMessage): Promise { return new Promise((resolve, reject) => { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { resolve(body); }); req.on('error', reject); }); } private async handleChatRequest(adapter: IProtocolAdapter, body: string, res: http.ServerResponse): Promise { try { const parsedRequest = adapter.parseRequest(body); const endpoints = await this.endpointProvider.getAllChatEndpoints(); if (endpoints.length === 0) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'No language models available' })); return; } const selectedEndpoint = this.selectEndpoint(endpoints, parsedRequest.model); if (!selectedEndpoint) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'No model found matching criteria' })); return; } // Set up streaming response res.writeHead(200, { 'Content-Type': adapter.getContentType(), 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }); // Create cancellation token for the request const tokenSource = new CancellationTokenSource(); // Handle client disconnect let requestComplete = false; res.on('close', () => { if (!requestComplete) { this.logService.info(`[LanguageModelServer] Client disconnected before request complete`); } tokenSource.cancel(); }); try { // Create streaming context with only essential shared data const context: IStreamingContext = { requestId: `req_${Math.random().toString(36).substr(2, 20)}`, endpoint: { modelId: selectedEndpoint.model, modelMaxPromptTokens: selectedEndpoint.modelMaxPromptTokens } }; // Send initial events if adapter supports them if (adapter.generateInitialEvents) { const initialEvents = adapter.generateInitialEvents(context); for (const event of initialEvents) { res.write(`event: ${event.event}\ndata: ${event.data}\n\n`); } } const userInitiatedRequest = parsedRequest.messages.at(-1)?.role === Raw.ChatRole.User; const fetchResult = await selectedEndpoint.makeChatRequest2({ debugName: 'agentLMServer' + (parsedRequest.type ? `-${parsedRequest.type}` : ''), messages: parsedRequest.messages as Raw.ChatMessage[], finishedCb: async (_fullText, _index, delta) => { if (tokenSource.token.isCancellationRequested) { return 0; // stop } // Emit text deltas if (delta.text) { const textData: IAgentStreamBlock = { type: 'text', content: delta.text }; for (const event of adapter.formatStreamResponse(textData, context)) { res.write(`event: ${event.event}\ndata: ${event.data}\n\n`); } } // Emit tool calls if present if (delta.copilotToolCalls && delta.copilotToolCalls.length > 0) { for (const call of delta.copilotToolCalls) { let input: object = {}; try { input = call.arguments ? JSON.parse(call.arguments) : {}; } catch { input = {}; } const toolData: IAgentStreamBlock = { type: 'tool_call', callId: call.id, name: call.name, input }; for (const event of adapter.formatStreamResponse(toolData, context)) { res.write(`event: ${event.event}\ndata: ${event.data}\n\n`); } } } return undefined; }, location: ChatLocation.Agent, requestOptions: { ...parsedRequest.options, stream: false }, userInitiatedRequest, telemetryProperties: { messageSource: `lmServer-${adapter.name}` } }, tokenSource.token); // Capture usage information if available let usage: APIUsage | undefined; if (fetchResult.type === ChatFetchResponseType.Success && fetchResult.usage) { usage = fetchResult.usage; } requestComplete = true; // Send final events const finalEvents = adapter.generateFinalEvents(context, usage); for (const event of finalEvents) { res.write(`event: ${event.event}\ndata: ${event.data}\n\n`); } res.end(); } catch (error) { requestComplete = true; if (error instanceof LanguageModelError) { res.write(JSON.stringify({ error: 'Language model error', code: error.code, message: error.message, cause: error.cause })); } else { res.write(JSON.stringify({ error: 'Request failed', message: error instanceof Error ? error.message : String(error) })); } res.end(); } finally { tokenSource.dispose(); } } catch (error) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Failed to process chat request', details: error instanceof Error ? error.message : String(error) })); } } private selectEndpoint(endpoints: readonly IChatEndpoint[], requestedModel?: string): IChatEndpoint | undefined { if (requestedModel) { // Handle model mapping let mappedModel = requestedModel; if (requestedModel.startsWith('claude-haiku')) { mappedModel = 'claude-haiku-4.5'; } if (requestedModel.startsWith('claude-sonnet-4')) { mappedModel = 'claude-sonnet-4.5'; } if (requestedModel.startsWith('claude-opus-4')) { mappedModel = 'claude-opus-4.5'; } // Try to find exact match first let selectedEndpoint = endpoints.find(e => e.family === mappedModel || e.model === mappedModel); // If not found, try to find by partial match for Anthropic models if (!selectedEndpoint && requestedModel.startsWith('claude-haiku-4')) { selectedEndpoint = endpoints.find(e => e.model.includes('claude-haiku-4-5')) ?? endpoints.find(e => e.model.includes('claude')); } else if (!selectedEndpoint && requestedModel.startsWith('claude-sonnet-4')) { selectedEndpoint = endpoints.find(e => e.model.includes('claude-sonnet-4-5')) ?? endpoints.find(e => e.model.includes('claude')); } else if (!selectedEndpoint && requestedModel.startsWith('claude-opus-4')) { selectedEndpoint = endpoints.find(e => e.model.includes('claude-opus-4-5')) ?? endpoints.find(e => e.model.includes('claude')); } return selectedEndpoint; } // Use first available model if no criteria specified return endpoints[0]; } public async start(): Promise { return new Promise((resolve) => { this.server.listen(0, '127.0.0.1', () => { const address = this.server.address(); if (address && typeof address === 'object') { this.config = { ...this.config, port: address.port }; this.logService.trace(`Language Model Server started on http://localhost:${this.config.port}`); resolve(); } }); }); } public stop(): void { this.server.close(); } public getConfig(): ILanguageModelServerConfig { return { ...this.config }; } } ================================================ FILE: src/extension/agents/node/test/mockLanguageModelServer.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ILanguageModelServerConfig, LanguageModelServer } from '../langModelServer'; /** * Mock implementation of LanguageModelServer for unit tests. It avoids binding * sockets and returns a deterministic configuration. */ export class MockLanguageModelServer extends LanguageModelServer { private _cfg: ILanguageModelServerConfig = { port: 12345, nonce: 'test-nonce' }; override async start(): Promise { } setMockConfig(cfg: ILanguageModelServerConfig) { this._cfg = cfg; } override getConfig(): ILanguageModelServerConfig { return this._cfg; } } ================================================ FILE: src/extension/agents/node/test/openaiAdapter.spec.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as http from 'http'; import { describe, expect, it } from 'vitest'; import { OpenAIAdapterFactoryForSTests } from '../adapters/openaiAdapterForSTests'; import { Raw } from '@vscode/prompt-tsx'; describe('OpenAIAdapterFactory', () => { it('should create an OpenAI adapter instance', () => { const factory = new OpenAIAdapterFactoryForSTests(); const adapter = factory.createAdapter(); // Verify the adapter has the correct name expect(adapter.name).toBe('openai'); }); it('should parse a basic OpenAI request', () => { const factory = new OpenAIAdapterFactoryForSTests(); const adapter = factory.createAdapter(); const requestBody = { model: 'gpt-4o', messages: [ { role: 'user', content: 'Hello' } ], temperature: 0.7 }; const parsedRequest = adapter.parseRequest(JSON.stringify(requestBody)); expect(parsedRequest.model).toBe('gpt-4o'); expect(parsedRequest.messages).toHaveLength(1); expect(parsedRequest.messages[0]).toEqual({ role: Raw.ChatRole.User, content: [{ text: 'Hello', type: Raw.ChatCompletionContentPartKind.Text }] } satisfies Raw.UserChatMessage); expect(parsedRequest.options?.temperature).toBe(0.7); }); it('should parse an OpenAI request with tools', () => { const factory = new OpenAIAdapterFactoryForSTests(); const adapter = factory.createAdapter(); const requestBody = { model: 'gpt-4o', messages: [ { role: 'user', content: 'What is the weather?' } ], tools: [ { type: 'function', function: { name: 'get_weather', description: 'Get the current weather', parameters: { type: 'object', properties: { location: { type: 'string' } } } } } ] }; const parsedRequest = adapter.parseRequest(JSON.stringify(requestBody)); expect(parsedRequest.model).toBe('gpt-4o'); expect(parsedRequest.messages).toHaveLength(1); expect(parsedRequest.options?.tools).toBeDefined(); expect(parsedRequest.options?.tools).toHaveLength(1); }); it('should extract auth key from headers', () => { const factory = new OpenAIAdapterFactoryForSTests(); const adapter = factory.createAdapter(); const headers: http.IncomingHttpHeaders = { 'authorization': 'Bearer test-key-123' }; const authKey = adapter.extractAuthKey(headers); expect(authKey).toBe('test-key-123'); }); it('should format text stream response', () => { const factory = new OpenAIAdapterFactoryForSTests(); const adapter = factory.createAdapter(); const context = { requestId: 'test-request-id', endpoint: { modelId: 'gpt-4o', modelMaxPromptTokens: 128000 } }; const streamData = { type: 'text' as const, content: 'Hello, world!' }; let events = adapter.formatStreamResponse(streamData, context); expect(events).toHaveLength(0); events = adapter.generateFinalEvents(context); expect(events).toHaveLength(2); expect(events[0].event).toBe('message'); expect(events[0].data).toContain('Hello, world!'); expect(events[1].event).toBe('message'); expect(JSON.parse(events[1].data).choices).toEqual([{ 'index': 0, 'delta': { 'content': null }, 'finish_reason': 'stop' }]); }); it('should format tool call stream response', () => { const factory = new OpenAIAdapterFactoryForSTests(); const adapter = factory.createAdapter(); const context = { requestId: 'test-request-id', endpoint: { modelId: 'gpt-4o', modelMaxPromptTokens: 128000 } }; const streamData = { type: 'tool_call' as const, callId: 'call_123', name: 'get_weather', input: { location: 'Boston' } }; const events = adapter.formatStreamResponse(streamData, context); expect(events).toHaveLength(1); expect(events[0].event).toBe('message'); expect(events[0].data).toContain('get_weather'); expect(events[0].data).toContain('Boston'); }); it('should generate final events with usage', () => { const factory = new OpenAIAdapterFactoryForSTests(); const adapter = factory.createAdapter(); const context = { requestId: 'test-request-id', endpoint: { modelId: 'gpt-4o', modelMaxPromptTokens: 128000 } }; const usage = { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }; const events = adapter.generateFinalEvents(context, usage); expect(events).toHaveLength(1); expect(events[0].event).toBe('message'); expect(events[0].data).toContain('"prompt_tokens":10'); expect(events[0].data).toContain('"completion_tokens":20'); }); }); ================================================ FILE: src/extension/agents/vscode-node/agentCustomizationSkillProvider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; import { ILogService } from '../../../platform/log/common/logService'; import { BaseSkillProvider } from './baseSkillProvider'; const USER_PROMPTS_FOLDER_PLACEHOLDER = '{{USER_PROMPTS_FOLDER}}'; /** * Provides the built-in agent-customization skill that teaches agents * how to work with VS Code's customization system (instructions, prompts, agents, skills). */ export class AgentCustomizationSkillProvider extends BaseSkillProvider { private cachedContent: Uint8Array | undefined; constructor( @ILogService logService: ILogService, @IVSCodeExtensionContext extensionContext: IVSCodeExtensionContext, ) { super(logService, extensionContext, 'agent-customization'); } private getUserPromptsFolder(): string { const globalStorageUri = this.extensionContext.globalStorageUri; const userFolderUri = vscode.Uri.joinPath(globalStorageUri, '..', '..'); const userPromptsFolderUri = vscode.Uri.joinPath(userFolderUri, 'prompts'); return userPromptsFolderUri.fsPath; } protected override processTemplate(templateContent: string): string { const userPromptsFolder = this.getUserPromptsFolder(); this.logService.trace(`[AgentCustomizationSkillProvider] Injected user prompts folder: ${userPromptsFolder}`); return templateContent.replace(USER_PROMPTS_FOLDER_PLACEHOLDER, userPromptsFolder); } protected override async getSkillContentBytes(): Promise { if (this.cachedContent) { return this.cachedContent; } this.cachedContent = await super.getSkillContentBytes(); return this.cachedContent; } } ================================================ FILE: src/extension/agents/vscode-node/agentTypes.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ /** * Handoff configuration for agent transitions (e.g., Plan → Agent). */ export interface AgentHandoff { readonly label: string; readonly agent: string; readonly prompt: string; readonly send?: boolean; readonly showContinueOn?: boolean; readonly model?: string; } /** * Agent configuration for building .agent.md content. * Shared by PlanAgentProvider, AskAgentProvider, and other custom agents. */ export interface AgentConfig { readonly name: string; readonly description: string; readonly argumentHint: string; readonly tools: string[]; readonly model?: string | readonly string[]; readonly target?: string; readonly disableModelInvocation?: boolean; readonly userInvocable?: boolean; readonly agents?: string[]; readonly handoffs?: AgentHandoff[]; readonly body: string; } /** * Default read-only tools shared by Plan, Ask, and other agents. * These tools can only inspect the workspace — they never modify it. */ export const DEFAULT_READ_TOOLS: readonly string[] = [ 'search', 'read', 'web', 'vscode/memory', 'github/issue_read', 'github.vscode-pull-request-github/issue_fetch', 'github.vscode-pull-request-github/activePullRequest', 'execute/getTerminalOutput', 'execute/testFailure' ]; /** * Builds .agent.md content from a configuration object using string formatting. * No YAML library required — generates valid YAML frontmatter via string templates. */ export function buildAgentMarkdown(config: AgentConfig): string { const lines: string[] = ['---']; // Simple scalar fields lines.push(`name: ${config.name}`); lines.push(`description: ${config.description}`); lines.push(`argument-hint: ${config.argumentHint}`); // Model (optional) — supports a single string or a priority list of models if (config.model) { if (Array.isArray(config.model)) { const quoted = config.model.map(m => `'${m.replace(/'/g, '\'\'')}'`).join(', '); lines.push(`model: [${quoted}]`); } else { lines.push(`model: ${config.model}`); } } if (config.target) { lines.push(`target: ${config.target}`); } if (config.disableModelInvocation) { lines.push(`disable-model-invocation: true`); } if (config.userInvocable === false) { lines.push(`user-invocable: false`); } // Tools array - flow style for readability // Escape single quotes by doubling them (YAML spec) if (config.tools.length > 0) { const quotedTools = config.tools.map(t => `'${t.replace(/'/g, '\'\'')}'`).join(', '); lines.push(`tools: [${quotedTools}]`); } // Agents array - same format as tools (empty array = no subagents allowed) if (config.agents) { const quotedAgents = config.agents.map(a => `'${a.replace(/'/g, '\'\'')}'`).join(', '); lines.push(`agents: [${quotedAgents}]`); } // Handoffs - block style for complex nested objects // Escape prompts using single quotes (with doubled single quotes for internal quotes) if (config.handoffs && config.handoffs.length > 0) { lines.push('handoffs:'); for (const handoff of config.handoffs) { lines.push(` - label: ${handoff.label}`); lines.push(` agent: ${handoff.agent}`); lines.push(` prompt: '${handoff.prompt.replace(/'/g, '\'\'')}'`); if (handoff.send !== undefined) { lines.push(` send: ${handoff.send}`); } if (handoff.showContinueOn !== undefined) { lines.push(` showContinueOn: ${handoff.showContinueOn}`); } if (handoff.model !== undefined) { lines.push(` model: ${handoff.model}`); } } } lines.push('---'); lines.push(config.body); return lines.join('\n'); } ================================================ FILE: src/extension/agents/vscode-node/askAgentProvider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { AGENT_FILE_EXTENSION } from '../../../platform/customInstructions/common/promptTypes'; import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService'; import { ILogService } from '../../../platform/log/common/logService'; import { Disposable } from '../../../util/vs/base/common/lifecycle'; import { AgentConfig, buildAgentMarkdown, DEFAULT_READ_TOOLS } from './agentTypes'; /** * Base Ask agent configuration. * The Ask agent is read-only: it answers questions, explains code, and * provides information without modifying the workspace. */ const BASE_ASK_AGENT_CONFIG: AgentConfig = { name: 'Ask', description: 'Answers questions without making changes', argumentHint: 'Ask a question about your code or project', target: 'vscode', disableModelInvocation: true, agents: [], tools: [ ...DEFAULT_READ_TOOLS, 'vscode.mermaid-chat-features/renderMermaidDiagram', ], body: '' // Generated dynamically in buildCustomizedConfig }; /** * Provides the Ask agent dynamically with settings-based customization. * * The Ask agent is a read-only conversational mode for answering questions, * explaining code, and researching topics without making any edits to the * workspace. It uses an embedded configuration and generates .agent.md content * with settings-based customization (additional tools and model override). */ export class AskAgentProvider extends Disposable implements vscode.ChatCustomAgentProvider { readonly label = vscode.l10n.t('Ask Agent'); private static readonly CACHE_DIR = 'ask-agent'; private static readonly AGENT_FILENAME = `Ask${AGENT_FILE_EXTENSION}`; private readonly _onDidChangeCustomAgents = this._register(new vscode.EventEmitter()); readonly onDidChangeCustomAgents = this._onDidChangeCustomAgents.event; constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @IVSCodeExtensionContext private readonly _extensionContext: IVSCodeExtensionContext, @IFileSystemService private readonly _fileSystemService: IFileSystemService, @ILogService private readonly _logService: ILogService, ) { super(); this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(ConfigKey.AskAgentAdditionalTools.fullyQualifiedId) || e.affectsConfiguration(ConfigKey.AskAgentModel.fullyQualifiedId)) { this._onDidChangeCustomAgents.fire(); } })); } async provideCustomAgents( _context: unknown, _token: vscode.CancellationToken ): Promise { const config = this._buildCustomizedConfig(); const content = buildAgentMarkdown(config); const fileUri = await this._writeCacheFile(content); return [{ uri: fileUri }]; } private async _writeCacheFile(content: string): Promise { const cacheDir = vscode.Uri.joinPath( this._extensionContext.globalStorageUri, AskAgentProvider.CACHE_DIR ); try { await this._fileSystemService.stat(cacheDir); } catch { await this._fileSystemService.createDirectory(cacheDir); } const fileUri = vscode.Uri.joinPath(cacheDir, AskAgentProvider.AGENT_FILENAME); await this._fileSystemService.writeFile(fileUri, new TextEncoder().encode(content)); this._logService.trace(`[AskAgentProvider] Wrote agent file: ${fileUri.toString()}`); return fileUri; } static buildAgentBody(): string { return `You are an ASK AGENT — a knowledgeable assistant that answers questions, explains code, and provides information. Your job: understand the user's question → research the codebase as needed → provide a clear, thorough answer. You are strictly read-only: NEVER modify files or run commands that change state. - NEVER use file editing tools, terminal commands that modify state, or any write operations - Focus on answering questions, explaining concepts, and providing information - Use search and read tools to gather context from the codebase when needed - Provide code examples in your responses when helpful, but do NOT apply them - Use #tool:vscode/askQuestions to clarify ambiguous questions before researching - When the user's question is about code, reference specific files and symbols - If a question would require making changes, explain what changes would be needed but do NOT make them You can help with: - **Code explanation**: How does this code work? What does this function do? - **Architecture questions**: How is the project structured? How do components interact? - **Debugging guidance**: Why might this error occur? What could cause this behavior? - **Best practices**: What's the recommended approach for X? How should I structure Y? - **API and library questions**: How do I use this API? What does this method expect? - **Codebase navigation**: Where is X defined? Where is Y used? - **General programming**: Language features, algorithms, design patterns, etc. 1. **Understand** the question — identify what the user needs to know 2. **Research** the codebase if needed — use search and read tools to find relevant code 3. **Clarify** if the question is ambiguous — use #tool:vscode/askQuestions 4. **Answer** clearly — provide a well-structured response with references to relevant code `; } private _buildCustomizedConfig(): AgentConfig { const additionalTools = this._configurationService.getConfig(ConfigKey.AskAgentAdditionalTools); const modelOverride = this._configurationService.getConfig(ConfigKey.AskAgentModel); // Collect tools to add const toolsToAdd: string[] = [...additionalTools]; // Always include askQuestions tool (now provided by core) toolsToAdd.push('vscode/askQuestions'); // Merge additional tools (deduplicated) const tools = toolsToAdd.length > 0 ? [...new Set([...BASE_ASK_AGENT_CONFIG.tools, ...toolsToAdd])] : [...BASE_ASK_AGENT_CONFIG.tools]; return { ...BASE_ASK_AGENT_CONFIG, tools, body: AskAgentProvider.buildAgentBody(), ...(modelOverride ? { model: modelOverride } : {}), }; } } ================================================ FILE: src/extension/agents/vscode-node/baseSkillProvider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import { SKILL_FILENAME } from '../../../platform/customInstructions/common/promptTypes'; import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; import { ILogService } from '../../../platform/log/common/logService'; import { Disposable } from '../../../util/vs/base/common/lifecycle'; import { registerDynamicSkillFolder } from './skillFsProviderHelper'; /** * Base class for skill providers that serve a template-based SKILL.md with placeholder replacements. * * Handles constructor registration with the dynamic skill folder filesystem, * template loading from `assets/prompts/skills//SKILL.md`, * encoding, error handling, and the `provideSkills` contract. * * Subclasses implement {@link processTemplate} to perform their own placeholder replacements. */ export abstract class BaseSkillProvider extends Disposable implements vscode.ChatSkillProvider { protected readonly skillContentUri: vscode.Uri; private readonly _skillFolderName: string; constructor( protected readonly logService: ILogService, protected readonly extensionContext: IVSCodeExtensionContext, skillFolderName: string, ) { super(); this._skillFolderName = skillFolderName; const registration = registerDynamicSkillFolder( this.extensionContext, skillFolderName, () => this.getSkillContentBytes(), ); this.skillContentUri = registration.skillUri; this._register(registration.disposable); } /** * Process the raw template string with placeholder replacements. * Called each time the skill content is requested (unless the subclass caches). */ protected abstract processTemplate(templateContent: string): string | Promise; protected async getSkillContentBytes(): Promise { try { const skillTemplateUri = vscode.Uri.joinPath( this.extensionContext.extensionUri, 'assets', 'prompts', 'skills', this._skillFolderName, SKILL_FILENAME, ); const templateBytes = await vscode.workspace.fs.readFile(skillTemplateUri); const templateContent = new TextDecoder().decode(templateBytes); const processedContent = await this.processTemplate(templateContent); return new TextEncoder().encode(processedContent); } catch (error) { this.logService.error(`[${this.constructor.name}] Error reading skill template: ${error}`); return new Uint8Array(); } } async provideSkills(_context: unknown, token: vscode.CancellationToken): Promise { if (token.isCancellationRequested) { return []; } return [{ uri: this.skillContentUri }]; } } ================================================ FILE: src/extension/agents/vscode-node/editModeAgentProvider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import { AGENT_FILE_EXTENSION } from '../../../platform/customInstructions/common/promptTypes'; import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService'; import { ILogService } from '../../../platform/log/common/logService'; import { Disposable } from '../../../util/vs/base/common/lifecycle'; import { AgentConfig, buildAgentMarkdown } from './agentTypes'; const BASE_EDIT_MODE_AGENT_CONFIG: AgentConfig = { name: 'Edit', description: 'Edit-only mode restricted to the currently active file and any files explicitly attached in the request context.', argumentHint: 'Describe the edit to apply in the active or attached files', target: 'vscode', disableModelInvocation: true, userInvocable: true, tools: ['read', 'edit'], agents: [], handoffs: [ { label: 'Continue with Agent Mode', agent: 'agent', prompt: 'You are now switching to Agent Mode, where you can read and edit any file in the codebase. Continue with the task without the previous restrictions of Edit Mode.', send: true, }, ], body: `You are a focused allowlist editing agent. ## Rules - Allowed files are strictly: (1) the currently active file and (2) files explicitly attached in the request context. - Only read and edit files in that allowlist. - Create a new file only when the user explicitly asks to create that file. - Never create, delete, rename, or modify any file outside that allowlist. - Never propose or use terminal commands. - If a request requires touching files outside the allowlist, stop and explain that Edit Mode is restricted to the active file plus attached files. ## Workflow 1. Build the allowed-file set from context: active file + attached files. 2. Confirm every requested edit target is in that allowed-file set before editing, unless it is an explicitly user-requested new file creation. 3. Make the minimum required edits only within allowed files. 4. Summarize exactly what changed and list touched files. 5. If further changes are needed outside the allowlist, suggest switching to Agent Mode to complete the task without restrictions.` }; export class EditModeAgentProvider extends Disposable implements vscode.ChatCustomAgentProvider { readonly label = vscode.l10n.t('Edit Mode Agent'); private static readonly CACHE_DIR = 'edit-mode-agent'; private static readonly AGENT_FILENAME = `EditMode${AGENT_FILE_EXTENSION}`; constructor( @IVSCodeExtensionContext private readonly _extensionContext: IVSCodeExtensionContext, @IFileSystemService private readonly _fileSystemService: IFileSystemService, @ILogService private readonly _logService: ILogService, ) { super(); } async provideCustomAgents( _context: unknown, _token: vscode.CancellationToken ): Promise { const content = buildAgentMarkdown(BASE_EDIT_MODE_AGENT_CONFIG); const fileUri = await this._writeCacheFile(content); return [{ uri: fileUri }]; } private async _writeCacheFile(content: string): Promise { const cacheDir = vscode.Uri.joinPath( this._extensionContext.globalStorageUri, EditModeAgentProvider.CACHE_DIR ); try { await this._fileSystemService.stat(cacheDir); } catch { await this._fileSystemService.createDirectory(cacheDir); } const fileUri = vscode.Uri.joinPath(cacheDir, EditModeAgentProvider.AGENT_FILENAME); await this._fileSystemService.writeFile(fileUri, new TextEncoder().encode(content)); this._logService.trace(`[EditModeAgentProvider] Wrote agent file: ${fileUri.toString()}`); return fileUri; } } ================================================ FILE: src/extension/agents/vscode-node/exploreAgentProvider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { AGENT_FILE_EXTENSION } from '../../../platform/customInstructions/common/promptTypes'; import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService'; import { ILogService } from '../../../platform/log/common/logService'; import { Disposable } from '../../../util/vs/base/common/lifecycle'; import { AgentConfig, buildAgentMarkdown, DEFAULT_READ_TOOLS } from './agentTypes'; /** * Fallback model priority list for the Explore agent. * Passed as a YAML array; the runtime picks the first available model. */ const EXPLORE_AGENT_FALLBACK_MODELS: readonly string[] = [ 'Claude Haiku 4.5 (copilot)', 'Gemini 3 Flash (Preview) (copilot)', 'Auto (copilot)', ]; /** * Base Explore agent configuration. * The Explore agent is a read-only code research subagent that autonomously * digs through codebases using multiple search strategies. */ const BASE_EXPLORE_AGENT_CONFIG: AgentConfig = { name: 'Explore', description: 'Fast read-only codebase exploration and Q&A subagent. Prefer over manually chaining multiple search and file-reading operations to avoid cluttering the main conversation. Safe to call in parallel. Specify thoroughness: quick, medium, or thorough.', argumentHint: 'Describe WHAT you\'re looking for and desired thoroughness (quick/medium/thorough)', target: 'vscode', userInvocable: false, agents: [], tools: [ ...DEFAULT_READ_TOOLS, ], body: '' // Generated dynamically in buildCustomizedConfig }; /** * Provides the Explore agent dynamically with settings-based customization. * * The Explore agent is a read-only code research subagent designed to be * invoked by other agents (e.g., Plan) for autonomous codebase exploration. * It uses small/fast models by default and focuses on search-heavy workflows. */ export class ExploreAgentProvider extends Disposable implements vscode.ChatCustomAgentProvider { readonly label = vscode.l10n.t('Explore Agent'); private static readonly CACHE_DIR = 'explore-agent'; private static readonly AGENT_FILENAME = `Explore${AGENT_FILE_EXTENSION}`; private readonly _onDidChangeCustomAgents = this._register(new vscode.EventEmitter()); readonly onDidChangeCustomAgents = this._onDidChangeCustomAgents.event; constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @IVSCodeExtensionContext private readonly _extensionContext: IVSCodeExtensionContext, @IFileSystemService private readonly _fileSystemService: IFileSystemService, @ILogService private readonly _logService: ILogService, ) { super(); this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('chat.exploreAgent.defaultModel') || e.affectsConfiguration(ConfigKey.ExploreAgentModel.fullyQualifiedId)) { this._onDidChangeCustomAgents.fire(); } })); } async provideCustomAgents( _context: unknown, _token: vscode.CancellationToken ): Promise { const config = this._buildCustomizedConfig(); const content = buildAgentMarkdown(config); const fileUri = await this._writeCacheFile(content); return [{ uri: fileUri }]; } private async _writeCacheFile(content: string): Promise { const cacheDir = vscode.Uri.joinPath( this._extensionContext.globalStorageUri, ExploreAgentProvider.CACHE_DIR ); try { await this._fileSystemService.stat(cacheDir); } catch { await this._fileSystemService.createDirectory(cacheDir); } const fileUri = vscode.Uri.joinPath(cacheDir, ExploreAgentProvider.AGENT_FILENAME); await this._fileSystemService.writeFile(fileUri, new TextEncoder().encode(content)); this._logService.trace(`[ExploreAgentProvider] Wrote agent file: ${fileUri.toString()}`); return fileUri; } static buildAgentBody(): string { return `You are an exploration agent specialized in rapid codebase analysis and answering questions efficiently. ## Search Strategy - Go **broad to narrow**: 1. Start with glob patterns or semantic codesearch to discover relevant areas 2. Narrow with text search (regex) or usages (LSP) for specific symbols or patterns 3. Read files only when you know the path or need full context - Pay attention to provided agent instructions/rules/skills as they apply to areas of the codebase to better understand architecture and best practices. - Use the github repo tool to search references in external dependencies. ## Speed Principles Adapt search strategy based on the requested thoroughness level. **Bias for speed** — return findings as quickly as possible: - Parallelize independent tool calls (multiple greps, multiple reads) - Stop searching once you have sufficient context - Make targeted searches, not exhaustive sweeps ## Output Report findings directly as a message. Include: - Files with absolute links - Specific functions, types, or patterns that can be reused - Analogous existing features that serve as implementation templates - Clear answers to what was asked, not comprehensive overviews Remember: Your goal is searching efficiently through MAXIMUM PARALLELISM to report concise and clear answers.`; } private _buildCustomizedConfig(): AgentConfig { // Model selection priority: core config > extension config > fallback list // Empty string means "not set", so we explicitly check for truthy values const coreDefaultModel = this._configurationService.getNonExtensionConfig('chat.exploreAgent.defaultModel'); const extModel = this._configurationService.getConfig(ConfigKey.ExploreAgentModel); const model: string | readonly string[] = coreDefaultModel || extModel || EXPLORE_AGENT_FALLBACK_MODELS; return { ...BASE_EXPLORE_AGENT_CONFIG, body: ExploreAgentProvider.buildAgentBody(), model, }; } } ================================================ FILE: src/extension/agents/vscode-node/githubOrgChatResourcesService.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import { IAuthenticationService } from '../../../platform/authentication/common/authentication'; import { AGENT_FILE_EXTENSION, INSTRUCTION_FILE_EXTENSION, PromptsType } from '../../../platform/customInstructions/common/promptTypes'; import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService'; import { FileType } from '../../../platform/filesystem/common/fileTypes'; import { getGithubRepoIdFromFetchUrl, IGitService } from '../../../platform/git/common/gitService'; import { IOctoKitService } from '../../../platform/github/common/githubService'; import { ILogService } from '../../../platform/log/common/logService'; import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService'; import { Disposable, DisposableStore, IDisposable } from '../../../util/vs/base/common/lifecycle'; import { createDecorator } from '../../../util/vs/platform/instantiation/common/instantiation'; export interface IGitHubOrgChatResourcesService extends IDisposable { /** * Returns the organization that should be used for the current session. */ getPreferredOrganizationName(): Promise; /** * Creates a polling subscription with a custom interval. * The callback will be invoked at the specified interval. * @param intervalMs The polling interval in milliseconds * @param callback The callback to invoke on each poll cycle * @returns A disposable that stops the polling when disposed */ startPolling(intervalMs: number, callback: (orgName: string) => Promise): IDisposable; /** * Reads a specific cached resource. * @returns The content of the resource, or undefined if not found */ readCacheFile(type: PromptsType, orgName: string, filename: string): Promise; /** * Writes a resource to the cache. * @returns True if the content was changed, false if unchanged */ writeCacheFile(type: PromptsType, orgName: string, filename: string, content: string, options?: { checkForChanges?: boolean }): Promise; /** * Deletes all cached resources of specified type for an organization. * Optionally provide set of filenames to exclude from deletion. */ clearCache(type: PromptsType, orgName: string, exclude?: Set): Promise; /** * Lists all cached resources for a specific organization and type. * @returns The list of cached resources. */ listCachedFiles(type: PromptsType, orgName: string): Promise; } export const IGitHubOrgChatResourcesService = createDecorator('IGitHubPromptFileService'); /** * Maps PromptsType to the cache subdirectory name. */ function getCacheSubdirectory(type: PromptsType): string { switch (type) { case PromptsType.instructions: return 'instructions'; case PromptsType.agent: return 'agents'; default: throw new Error(`Unsupported PromptsType: ${type}`); } } /** * Returns true if the filename is valid for the given PromptsType. */ function isValidFile(type: PromptsType, fileName: string): boolean { switch (type) { case PromptsType.instructions: return fileName.endsWith(INSTRUCTION_FILE_EXTENSION); case PromptsType.agent: return fileName.endsWith(AGENT_FILE_EXTENSION); default: throw new Error(`Unsupported PromptsType: ${type}`); } } export class GitHubOrgChatResourcesService extends Disposable implements IGitHubOrgChatResourcesService { private static readonly CACHE_ROOT = 'github'; // private readonly _pollingSubscriptions = this._register(new DisposableStore()); private _cachedPreferredOrgName: Promise | undefined; constructor( @IAuthenticationService private readonly authService: IAuthenticationService, @IVSCodeExtensionContext private readonly extensionContext: IVSCodeExtensionContext, @IFileSystemService private readonly fileSystem: IFileSystemService, @IGitService private readonly gitService: IGitService, @ILogService private readonly logService: ILogService, @IOctoKitService private readonly octoKitService: IOctoKitService, @IWorkspaceService private readonly workspaceService: IWorkspaceService, ) { super(); // Invalidate cached org name when workspace folders change this._register(this.workspaceService.onDidChangeWorkspaceFolders(() => { this.logService.trace('[GitHubOrgChatResourcesService] Workspace folders changed, invalidating cached org name'); this._cachedPreferredOrgName = undefined; })); // Invalidate cached org name when authentication changes (sign in/out) this._register(this.authService.onDidAuthenticationChange(() => { this.logService.trace('[GitHubOrgChatResourcesService] Authentication changed, invalidating cached org name'); this._cachedPreferredOrgName = undefined; })); } async getPreferredOrganizationName(): Promise { if (!this._cachedPreferredOrgName) { this._cachedPreferredOrgName = this.computePreferredOrganizationName(); } return this._cachedPreferredOrgName; } private async computePreferredOrganizationName(): Promise { // Check if user is signed in first const currentUser = await this.octoKitService.getCurrentAuthedUser(); if (!currentUser) { this.logService.trace('[GitHubOrgChatResourcesService] User is not signed in'); return undefined; } // Use the organization from the current workspace's git repository, if any const workspaceOrg = await this.getWorkspaceRepositoryOrganization(); this.logService.trace(`[GitHubOrgChatResourcesService] Workspace organization: ${workspaceOrg ?? 'none'}`); if (workspaceOrg) { this.logService.trace(`[GitHubOrgChatResourcesService] Using workspace organization: ${workspaceOrg}`); return workspaceOrg; } // Check if user has Copilot access through an organization (Business/Enterprise subscription) // and prefer that organization if available const copilotOrganizations = this.authService.copilotToken?.organizationLoginList ?? []; this.logService.trace(`[GitHubOrgChatResourcesService] Copilot organizations: ${JSON.stringify(copilotOrganizations)}`); if (copilotOrganizations.length > 0) { const copilotOrg = copilotOrganizations[0]; this.logService.trace(`[GitHubOrgChatResourcesService] Using Copilot sign-in organization: ${copilotOrg}`); return copilotOrg; } // Fall back to the first organization the user belongs to // Get the organizations the user is a member of let userOrganizations: string[]; try { userOrganizations = await this.octoKitService.getUserOrganizations({ createIfNone: false }, 1); this.logService.trace(`[GitHubOrgChatResourcesService] User organizations: ${JSON.stringify(userOrganizations)}`); if (userOrganizations.length === 0) { this.logService.trace('[GitHubOrgChatResourcesService] No organizations found for user'); return undefined; } } catch (error) { this.logService.error(`[GitHubOrgChatResourcesService] Error getting user organizations: ${error}`); return undefined; } this.logService.trace(`[GitHubOrgChatResourcesService] Falling back to first user organization: ${userOrganizations[0]}`); return userOrganizations[0]; } /** * Gets the organization from the current workspace's git repository, if any. */ private async getWorkspaceRepositoryOrganization(): Promise { const workspaceFolders = this.workspaceService.getWorkspaceFolders(); if (workspaceFolders.length === 0) { return undefined; } try { // TODO: Support multi-root workspaces by checking all folders. // This would need workspace-aware context for deciding when to use which org, which is currently not in scope. const repoInfo = await this.gitService.getRepositoryFetchUrls(workspaceFolders[0]); if (!repoInfo?.remoteFetchUrls?.length) { return undefined; } // Try each remote URL to find a GitHub repo for (const fetchUrl of repoInfo.remoteFetchUrls) { if (!fetchUrl) { continue; } const repoId = getGithubRepoIdFromFetchUrl(fetchUrl); if (repoId) { this.logService.trace(`[GitHubOrgChatResourcesService] Found GitHub repo: ${repoId.org}/${repoId.repo}`); return repoId.org; } } } catch (error) { this.logService.trace(`[GitHubOrgChatResourcesService] Error getting workspace repository: ${error}`); } return undefined; } startPolling(intervalMs: number, callback: (orgName: string) => Promise): IDisposable { const disposables = new DisposableStore(); let isPolling = false; const poll = async () => { if (isPolling) { return; } isPolling = true; try { const orgName = await this.getPreferredOrganizationName(); if (orgName) { try { await callback(orgName); } catch (error) { this.logService.error(`[GitHubOrgChatResourcesService] Error in polling callback: ${error}`); } } } finally { isPolling = false; } }; // Initial poll void poll(); // TODO: re-enable polling // Set up interval polling // const intervalId = setInterval(() => poll(), intervalMs); // disposables.add(toDisposable(() => clearInterval(intervalId))); // this._pollingSubscriptions.add(disposables); return disposables; } private getCacheDir(orgName: string, type: PromptsType): vscode.Uri { const sanitizedOrg = this.sanitizeFilename(orgName); const subdirectory = getCacheSubdirectory(type); return vscode.Uri.joinPath( this.extensionContext.globalStorageUri, GitHubOrgChatResourcesService.CACHE_ROOT, sanitizedOrg, subdirectory ); } private getCacheFileUri(orgName: string, type: PromptsType, filename: string): vscode.Uri { return vscode.Uri.joinPath(this.getCacheDir(orgName, type), filename); } private sanitizeFilename(name: string): string { return name.replace(/[^a-z0-9_-]/gi, '_').toLowerCase(); } private async ensureCacheDir(orgName: string, type: PromptsType): Promise { const cacheDir = this.getCacheDir(orgName, type); try { await this.fileSystem.stat(cacheDir); } catch { // createDirectory should create parent directories recursively await this.fileSystem.createDirectory(cacheDir); } } async readCacheFile(type: PromptsType, orgName: string, filename: string): Promise { try { const fileUri = this.getCacheFileUri(orgName, type, filename); const content = await this.fileSystem.readFile(fileUri); return new TextDecoder().decode(content); } catch { this.logService.error(`[GitHubOrgChatResourcesService] Cache file not found: ${filename}`); return undefined; } } async writeCacheFile(type: PromptsType, orgName: string, filename: string, content: string, options?: { checkForChanges?: boolean }): Promise { await this.ensureCacheDir(orgName, type); const fileUri = this.getCacheFileUri(orgName, type, filename); const contentBytes = new TextEncoder().encode(content); // Check for changes if requested let hasChanges = true; if (options?.checkForChanges) { try { hasChanges = false; // First check file size to avoid reading file if size differs const stat = await this.fileSystem.stat(fileUri); if (stat.size !== contentBytes.length) { hasChanges = true; } // Sizes match, need to compare content const existingContent = await this.fileSystem.readFile(fileUri); const existingText = new TextDecoder().decode(existingContent); if (existingText !== content) { this.logService.trace(`[GitHubOrgChatResourcesService] Skipped writing cache file: ${fileUri.toString()}`); hasChanges = true; } else { // Content is the same, no need to write return false; } } catch { // File doesn't exist, so we have changes hasChanges = true; } } await this.fileSystem.writeFile(fileUri, contentBytes); this.logService.trace(`[GitHubOrgChatResourcesService] Wrote cache file: ${fileUri.toString()}`); return hasChanges; } async clearCache(type: PromptsType, orgName: string, exclude?: Set): Promise { const cacheDir = this.getCacheDir(orgName, type); try { const files = await this.fileSystem.readDirectory(cacheDir); for (const [filename, fileType] of files) { if (fileType === FileType.File && isValidFile(type, filename) && !exclude?.has(filename)) { await this.fileSystem.delete(vscode.Uri.joinPath(cacheDir, filename)); this.logService.trace(`[GitHubOrgChatResourcesService] Deleted cache file: ${filename}`); } } } catch { // Directory might not exist } } async listCachedFiles(type: PromptsType, orgName: string): Promise { const resources: vscode.ChatResource[] = []; const cacheDir = this.getCacheDir(orgName, type); try { const files = await this.fileSystem.readDirectory(cacheDir); for (const [filename, fileType] of files) { if (fileType === FileType.File && isValidFile(type, filename)) { const fileUri = vscode.Uri.joinPath(cacheDir, filename); resources.push({ uri: fileUri }); } } } catch { // Directory might not exist yet this.logService.trace(`[GitHubOrgChatResourcesService] Cache directory does not exist: ${cacheDir.toString()}`); } return resources; } } ================================================ FILE: src/extension/agents/vscode-node/githubOrgCustomAgentProvider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import YAML, { Scalar } from 'yaml'; import { AGENT_FILE_EXTENSION, PromptsType } from '../../../platform/customInstructions/common/promptTypes'; import { CustomAgentDetails, CustomAgentListOptions, IOctoKitService } from '../../../platform/github/common/githubService'; import { ILogService } from '../../../platform/log/common/logService'; import { Disposable } from '../../../util/vs/base/common/lifecycle'; import { IGitHubOrgChatResourcesService } from './githubOrgChatResourcesService'; /** * Polling interval for refreshing custom agents from GitHub (5 minutes). * We poll a bit less frequently as we need to loop and fetch full agent details including prompt content. */ const REFRESH_INTERVAL_MS = 5 * 60 * 1000; export class GitHubOrgCustomAgentProvider extends Disposable implements vscode.ChatCustomAgentProvider { private readonly _onDidChangeCustomAgents = this._register(new vscode.EventEmitter()); readonly onDidChangeCustomAgents = this._onDidChangeCustomAgents.event; constructor( @IOctoKitService private readonly octoKitService: IOctoKitService, @ILogService private readonly logService: ILogService, @IGitHubOrgChatResourcesService private readonly githubOrgChatResourcesService: IGitHubOrgChatResourcesService, ) { super(); // Set up polling with provider-specific interval this._register(this.githubOrgChatResourcesService.startPolling(REFRESH_INTERVAL_MS, this.pollAgents.bind(this))); } async provideCustomAgents(_context: unknown, token: vscode.CancellationToken): Promise { try { const orgId = await this.githubOrgChatResourcesService.getPreferredOrganizationName(); if (!orgId) { this.logService.trace('[GitHubOrgCustomAgentProvider] No organization available for providing agents'); return []; } if (token.isCancellationRequested) { this.logService.trace('[GitHubOrgCustomAgentProvider] provideCustomAgents was cancelled'); return []; } return await this.githubOrgChatResourcesService.listCachedFiles(PromptsType.agent, orgId); } catch (error) { this.logService.error(`[GitHubOrgCustomAgentProvider] Error reading from cache: ${error}`); return []; } } private async pollAgents(orgId: string): Promise { try { // Convert VS Code API options to internal options // It's okay to include enterprise agents here which may take from other orgs, as we only retrieve per org const internalOptions = { includeSources: ['org', 'enterprise'] } satisfies CustomAgentListOptions; // Note: we need to fetch an arbitrary visible/accessible repository, in case user does not have access to .github-private const repos = await this.octoKitService.getOrganizationRepositories(orgId, { createIfNone: false }, 1); if (repos.length === 0) { this.logService.trace(`[GitHubOrgCustomAgentProvider] No repositories found for org ${orgId}`); return; } // Fetch custom agents from GitHub and compare with existing agents in cache const repoName = repos[0]; const [agents, existingAgents] = await Promise.all([ this.octoKitService.getCustomAgents(orgId, repoName, internalOptions, { createIfNone: false }), this.githubOrgChatResourcesService.listCachedFiles(PromptsType.agent, orgId) ]); let hasChanges: boolean = existingAgents.length !== agents.length; const newFiles = new Set(); for (const agent of agents) { // Fetch full agent details including prompt content const agentDetails = await this.octoKitService.getCustomAgentDetails( agent.repo_owner, agent.repo_name, agent.name, agent.version, { createIfNone: false }, ); // Generate agent markdown file content if (agentDetails) { const filename = `${agent.name}${AGENT_FILE_EXTENSION}`; const content = this.generateAgentMarkdown(agentDetails); const result = await this.githubOrgChatResourcesService.writeCacheFile( PromptsType.agent, orgId, filename, content, { checkForChanges: !hasChanges } ); hasChanges ||= result; newFiles.add(filename); } } if (!hasChanges) { this.logService.trace('[GitHubOrgCustomAgentProvider] No changes detected in cache'); return; } // Remove all cached agents that are no longer present await this.githubOrgChatResourcesService.clearCache(PromptsType.agent, orgId, newFiles); // Fire event to notify consumers that agents have changed this._onDidChangeCustomAgents.fire(); } catch (error) { this.logService.error(`[GitHubOrgCustomAgentProvider] Error polling for agents: ${error}`); } } private generateAgentMarkdown(agent: CustomAgentDetails): string { const frontmatterObj: Record = {}; if (agent.display_name) { frontmatterObj.name = yamlString(agent.display_name); } if (agent.description) { frontmatterObj.description = yamlString(agent.description); } if (agent.tools && agent.tools.length > 0 && agent.tools[0] !== '*') { frontmatterObj.tools = agent.tools; } if (agent.argument_hint) { frontmatterObj['argument-hint'] = agent.argument_hint; } if (agent.target) { frontmatterObj.target = agent.target; } if (agent.model) { frontmatterObj.model = agent.model; } if (agent.disable_model_invocation !== undefined) { frontmatterObj['disable-model-invocation'] = agent.disable_model_invocation; } if (agent.user_invocable !== undefined) { frontmatterObj['user-invocable'] = agent.user_invocable; } const frontmatter = YAML.stringify(frontmatterObj, { lineWidth: 0, // Force double-quoted strings with newlines to use escape sequences rather than multi-line blocks. // The custom YAML parser doesn't support multi-line strings. doubleQuotedMinMultiLineLength: Infinity, }).trim(); const body = agent.prompt ?? ''; return `---\n${frontmatter}\n---\n${body}\n`; } } /** * Returns a YAML-safe value for a string. If the string contains characters * that need quoting (like #, :, etc.), wraps it in a Scalar with appropriate quoting. * The custom YAML parser doesn't handle escape sequences, so we prefer single quotes * unless the value contains single quotes or newlines (in which case we use double quotes). */ export function yamlString(value: string): string | Scalar { // Characters/patterns that require quoting in YAML values: // - # starts a comment, : is key-value separator, [] {} are collection syntax, , is separator // - Values starting with quotes need quoting to preserve as strings // - Values with leading/trailing whitespace need quoting // - Boolean keywords (true, false) would be parsed as booleans // - Null keywords (null, ~) would be parsed as null // - Numeric-looking strings would be parsed as numbers // - Newlines would corrupt the value (parser splits on newlines) // - Single quotes in value require double quotes (parser doesn't handle escapes) const needsQuoting = /[#:\[\]{},\n\r]/.test(value) || value.startsWith('\'') || value.startsWith('"') || value !== value.trim() || value === 'true' || value === 'false' || value === 'null' || value === '~' || looksLikeNumber(value); if (needsQuoting) { const scalar = new Scalar(value); // Use double quotes if value contains single quotes OR newlines. // - Single quotes can't be escaped in YAML single-quoted strings // - Newlines in single-quoted strings become multi-line blocks, but the custom // YAML parser doesn't support multi-line strings. Double quotes preserve // newlines as \n escape sequences. scalar.type = (value.includes('\'') || value.includes('\n') || value.includes('\r')) ? Scalar.QUOTE_DOUBLE : Scalar.QUOTE_SINGLE; return scalar; } return value; } /** * Checks if a string looks like a number that would be parsed as a numeric value. * Matches the logic in the custom YAML parser's isValidNumber and createValueNode. */ export function looksLikeNumber(value: string): boolean { if (value === '') { return false; } const num = Number(value); // Matches parser logic: !isNaN && isFinite && passes regex /^-?\d*\.?\d+$/ return !isNaN(num) && isFinite(num) && /^-?\d*\.?\d+$/.test(value); } ================================================ FILE: src/extension/agents/vscode-node/githubOrgInstructionsProvider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import { INSTRUCTION_FILE_EXTENSION, PromptsType } from '../../../platform/customInstructions/common/promptTypes'; import { IOctoKitService } from '../../../platform/github/common/githubService'; import { ILogService } from '../../../platform/log/common/logService'; import { Disposable } from '../../../util/vs/base/common/lifecycle'; import { IGitHubOrgChatResourcesService } from './githubOrgChatResourcesService'; const INSTRUCTIONS_BASE_FILE_NAME = 'default'; const REFRESH_INTERVAL_MS = 2 * 60 * 1000; export class GitHubOrgInstructionsProvider extends Disposable implements vscode.ChatInstructionsProvider { private readonly _onDidChangeInstructions = this._register(new vscode.EventEmitter()); readonly onDidChangeInstructions = this._onDidChangeInstructions.event; constructor( @ILogService private readonly logService: ILogService, @IOctoKitService private readonly octoKitService: IOctoKitService, @IGitHubOrgChatResourcesService private readonly githubOrgChatResourcesService: IGitHubOrgChatResourcesService, ) { super(); // Set up polling with provider-specific interval this._register(this.githubOrgChatResourcesService.startPolling(REFRESH_INTERVAL_MS, this.pollInstructions.bind(this))); } async provideInstructions( _options: unknown, token: vscode.CancellationToken ): Promise { try { const orgId = await this.githubOrgChatResourcesService.getPreferredOrganizationName(); if (!orgId) { this.logService.trace('[GitHubOrgInstructionsProvider] No organization available for providing agents'); return []; } if (token.isCancellationRequested) { this.logService.trace('[GitHubOrgInstructionsProvider] provideCustomAgents was cancelled'); return []; } return await this.githubOrgChatResourcesService.listCachedFiles(PromptsType.instructions, orgId); } catch (error) { this.logService.error(`[GitHubOrgInstructionsProvider] Error reading from cache: ${error}`); return []; } } private async pollInstructions(orgId: string): Promise { try { const instructions = await this.octoKitService.getOrgCustomInstructions(orgId, { createIfNone: false }); if (!instructions) { await this.githubOrgChatResourcesService.clearCache(PromptsType.instructions, orgId); this.logService.trace(`[GitHubOrgInstructionsProvider] No custom instructions found for org ${orgId}`); return; } // Write the instructions to cache const fileName = `${INSTRUCTIONS_BASE_FILE_NAME}${INSTRUCTION_FILE_EXTENSION}`; const content = `--- applyTo: '**' --- ${instructions}`; const contentChanged = await this.githubOrgChatResourcesService.writeCacheFile(PromptsType.instructions, orgId, fileName, content, { checkForChanges: true }); // If no changes, we can return if (!contentChanged) { this.logService.trace(`[GitHubOrgInstructionsProvider] No changes detected in cache for org ${orgId}`); return; } // Otherwise, fire event to notify consumers that instructions have changed this._onDidChangeInstructions.fire(); } catch (error) { this.logService.error(`[GitHubOrgInstructionsProvider] Error polling for instructions: ${error}`); } } } ================================================ FILE: src/extension/agents/vscode-node/planAgentProvider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { AGENT_FILE_EXTENSION } from '../../../platform/customInstructions/common/promptTypes'; import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService'; import { ILogService } from '../../../platform/log/common/logService'; import { Disposable } from '../../../util/vs/base/common/lifecycle'; import { AgentConfig, AgentHandoff, buildAgentMarkdown, DEFAULT_READ_TOOLS } from './agentTypes'; /** * Base Plan agent configuration - embedded from Plan.agent.md * This avoids runtime file loading and YAML parsing dependencies. */ const BASE_PLAN_AGENT_CONFIG: AgentConfig = { name: 'Plan', description: 'Researches and outlines multi-step plans', argumentHint: 'Outline the goal or problem to research', target: 'vscode', disableModelInvocation: true, agents: ['Explore'], tools: [ ...DEFAULT_READ_TOOLS, 'agent', ], handoffs: [], // Handoffs are generated dynamically in buildCustomizedConfig body: '' // Body is generated dynamically in buildCustomizedConfig }; /** * Provides the Plan agent dynamically with settings-based customization. * * This provider uses an embedded configuration and generates .agent.md content * with settings-based customization (additional tools and model override). * No external file loading or YAML parsing dependencies required. */ export class PlanAgentProvider extends Disposable implements vscode.ChatCustomAgentProvider { readonly label = vscode.l10n.t('Plan Agent'); private static readonly CACHE_DIR = 'plan-agent'; private static readonly AGENT_FILENAME = `Plan${AGENT_FILE_EXTENSION}`; private readonly _onDidChangeCustomAgents = this._register(new vscode.EventEmitter()); readonly onDidChangeCustomAgents = this._onDidChangeCustomAgents.event; constructor( @IConfigurationService private readonly configurationService: IConfigurationService, @IVSCodeExtensionContext private readonly extensionContext: IVSCodeExtensionContext, @IFileSystemService private readonly fileSystemService: IFileSystemService, @ILogService private readonly logService: ILogService, ) { super(); // Listen for settings changes to refresh agents // Note: When settings change, we fire onDidChangeCustomAgents which causes VS Code to re-fetch // the agent definition. However, handoff buttons already rendered may not work as // these capture the model at render time. this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(ConfigKey.PlanAgentAdditionalTools.fullyQualifiedId) || e.affectsConfiguration(ConfigKey.Deprecated.PlanAgentModel.fullyQualifiedId) || e.affectsConfiguration('chat.planAgent.defaultModel') || e.affectsConfiguration(ConfigKey.ImplementAgentModel.fullyQualifiedId)) { this._onDidChangeCustomAgents.fire(); } })); } async provideCustomAgents( _context: unknown, _token: vscode.CancellationToken ): Promise { // Build config with settings-based customization const config = this.buildCustomizedConfig(); // Generate .agent.md content const content = buildAgentMarkdown(config); // Write to cache file and return URI const fileUri = await this.writeCacheFile(content); return [{ uri: fileUri }]; } private async writeCacheFile(content: string): Promise { const cacheDir = vscode.Uri.joinPath( this.extensionContext.globalStorageUri, PlanAgentProvider.CACHE_DIR ); // Ensure cache directory exists try { await this.fileSystemService.stat(cacheDir); } catch { await this.fileSystemService.createDirectory(cacheDir); } const fileUri = vscode.Uri.joinPath(cacheDir, PlanAgentProvider.AGENT_FILENAME); await this.fileSystemService.writeFile(fileUri, new TextEncoder().encode(content)); this.logService.trace(`[PlanAgentProvider] Wrote agent file: ${fileUri.toString()}`); return fileUri; } static buildAgentBody(): string { const discoverySection = `## 1. Discovery Run the *Explore* subagent to gather context, analogous existing features to use as implementation templates, and potential blockers or ambiguities. When the task spans multiple independent areas (e.g., frontend + backend, different features, separate repos), launch **2-3 *Explore* subagents in parallel** — one per area — to speed up discovery. Update the plan with your findings.`; return `You are a PLANNING AGENT, pairing with the user to create a detailed, actionable plan. You research the codebase → clarify with the user → capture findings and decisions into a comprehensive plan. This iterative approach catches edge cases and non-obvious requirements BEFORE implementation begins. Your SOLE responsibility is planning. NEVER start implementation. **Current plan**: \`/memories/session/plan.md\` - update using #tool:vscode/memory. - STOP if you consider running file editing tools — plans are for others to execute. The only write tool you have is #tool:vscode/memory for persisting plans. - Use #tool:vscode/askQuestions freely to clarify requirements — don't make large assumptions - Present a well-researched plan with loose ends tied BEFORE implementation Cycle through these phases based on user input. This is iterative, not linear. If the user task is highly ambiguous, do only *Discovery* to outline a draft plan, then move on to alignment before fleshing out the full plan. ${discoverySection} ## 2. Alignment If research reveals major ambiguities or if you need to validate assumptions: - Use #tool:vscode/askQuestions to clarify intent with the user. - Surface discovered technical constraints or alternative approaches - If answers significantly change the scope, loop back to **Discovery** ## 3. Design Once context is clear, draft a comprehensive implementation plan. The plan should reflect: - Structured concise enough to be scannable and detailed enough for effective execution - Step-by-step implementation with explicit dependencies — mark which steps can run in parallel vs. which block on prior steps - For plans with many steps, group into named phases that are each independently verifiable - Verification steps for validating the implementation, both automated and manual - Critical architecture to reuse or use as reference — reference specific functions, types, or patterns, not just file names - Critical files to be modified (with full paths) - Explicit scope boundaries — what's included and what's deliberately excluded - Reference decisions from the discussion - Leave no ambiguity Save the comprehensive plan document to \`/memories/session/plan.md\` via #tool:vscode/memory, then show the scannable plan to the user for review. You MUST show plan to the user, as the plan file is for persistence only, not a substitute for showing it to the user. ## 4. Refinement On user input after showing the plan: - Changes requested → revise and present updated plan. Update \`/memories/session/plan.md\` to keep the documented plan in sync - Questions asked → clarify, or use #tool:vscode/askQuestions for follow-ups - Alternatives wanted → loop back to **Discovery** with new subagent - Approval given → acknowledge, the user can now use handoff buttons Keep iterating until explicit approval or handoff. \`\`\`markdown ## Plan: {Title (2-10 words)} {TL;DR - what, why, and how (your recommended approach).} **Steps** 1. {Implementation step-by-step — note dependency ("*depends on N*") or parallelism ("*parallel with step N*") when applicable} 2. {For plans with 5+ steps, group steps into named phases with enough detail to be independently actionable} **Relevant files** - \`{full/path/to/file}\` — {what to modify or reuse, referencing specific functions/patterns} **Verification** 1. {Verification steps for validating the implementation (**Specific** tasks, tests, commands, MCP tools, etc; not generic statements)} **Decisions** (if applicable) - {Decision, assumptions, and includes/excluded scope} **Further Considerations** (if applicable, 1-3 items) 1. {Clarifying question with recommendation. Option A / Option B / Option C} 2. {…} \`\`\` Rules: - NO code blocks — describe changes, link to files and specific symbols/functions - NO blocking questions at the end — ask during workflow via #tool:vscode/askQuestions - The plan MUST be presented to the user, don't just mention the plan file. `; } private buildCustomizedConfig(): AgentConfig { const additionalTools = this.configurationService.getConfig(ConfigKey.PlanAgentAdditionalTools); const coreDefaultModel = this.configurationService.getNonExtensionConfig('chat.planAgent.defaultModel'); const modelOverride = coreDefaultModel || this.configurationService.getConfig(ConfigKey.Deprecated.PlanAgentModel); const implementAgentModelOverride = this.configurationService.getConfig(ConfigKey.ImplementAgentModel); // Build handoffs dynamically with model override const startImplementationHandoff: AgentHandoff = { label: 'Start Implementation', agent: 'agent', prompt: 'Start implementation', send: true, ...(implementAgentModelOverride ? { model: implementAgentModelOverride } : {}) }; const openInEditorHandoff: AgentHandoff = { label: 'Open in Editor', agent: 'agent', prompt: '#createFile the plan as is into an untitled file (`untitled:plan-${camelCaseName}.prompt.md` without frontmatter) for further refinement.', showContinueOn: false, send: true }; // Collect tools to add const toolsToAdd: string[] = [...additionalTools]; // Always include askQuestions tool (now provided by core) toolsToAdd.push('vscode/askQuestions'); // Merge additional tools (deduplicated) const tools = toolsToAdd.length > 0 ? [...new Set([...BASE_PLAN_AGENT_CONFIG.tools, ...toolsToAdd])] : [...BASE_PLAN_AGENT_CONFIG.tools]; // Start with base config return { ...BASE_PLAN_AGENT_CONFIG, tools, handoffs: [startImplementationHandoff, openInEditorHandoff, ...(BASE_PLAN_AGENT_CONFIG.handoffs ?? [])], body: PlanAgentProvider.buildAgentBody(), ...(modelOverride ? { model: modelOverride } : {}), }; } } ================================================ FILE: src/extension/agents/vscode-node/promptFileContrib.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { Disposable, MutableDisposable } from '../../../util/vs/base/common/lifecycle'; import { SyncDescriptor } from '../../../util/vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { IExtensionContribution } from '../../common/contributions'; import { AgentCustomizationSkillProvider } from './agentCustomizationSkillProvider'; import { AskAgentProvider } from './askAgentProvider'; import { EditModeAgentProvider } from './editModeAgentProvider'; import { ExploreAgentProvider } from './exploreAgentProvider'; import { GitHubOrgCustomAgentProvider } from './githubOrgCustomAgentProvider'; import { GitHubOrgInstructionsProvider } from './githubOrgInstructionsProvider'; import { PlanAgentProvider } from './planAgentProvider'; import { TroubleshootSkillProvider } from './troubleshootSkillProvider'; export class PromptFileContribution extends Disposable implements IExtensionContribution { readonly id = 'PromptFiles'; constructor( @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, ) { super(); // Register custom agent provider if ('registerCustomAgentProvider' in vscode.chat) { const editModeProviderRegistration = this._register(new MutableDisposable()); const editModeHiddenSetting = 'chat.editMode.hidden'; const updateEditModeProvider = () => { const isEditModeHidden = configurationService.getNonExtensionConfig(editModeHiddenSetting); if (!isEditModeHidden) { if (!editModeProviderRegistration.value) { editModeProviderRegistration.value = vscode.chat.registerCustomAgentProvider(instantiationService.createInstance(EditModeAgentProvider)); } } else { editModeProviderRegistration.clear(); } }; updateEditModeProvider(); this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(editModeHiddenSetting)) { updateEditModeProvider(); } })); // Only register the provider if the setting is enabled if (configurationService.getConfig(ConfigKey.EnableOrganizationCustomAgents)) { const githubOrgAgentProvider: vscode.ChatCustomAgentProvider = instantiationService.createInstance(new SyncDescriptor(GitHubOrgCustomAgentProvider)); this._register(vscode.chat.registerCustomAgentProvider(githubOrgAgentProvider)); } // Register Plan agent provider for dynamic settings-based customization const planProvider = instantiationService.createInstance(PlanAgentProvider); this._register(vscode.chat.registerCustomAgentProvider(planProvider)); // Register Ask agent provider for read-only Q&A mode const askProvider = instantiationService.createInstance(AskAgentProvider); this._register(vscode.chat.registerCustomAgentProvider(askProvider)); // Register Explore agent provider for code research subagent const exploreProvider = instantiationService.createInstance(ExploreAgentProvider); this._register(vscode.chat.registerCustomAgentProvider(exploreProvider)); } // Register instructions provider if ('registerInstructionsProvider' in vscode.chat) { // Only register the provider if the setting is enabled if (configurationService.getConfig(ConfigKey.EnableOrganizationInstructions)) { const githubOrgInstructionsProvider: vscode.ChatInstructionsProvider = instantiationService.createInstance(new SyncDescriptor(GitHubOrgInstructionsProvider)); this._register(vscode.chat.registerInstructionsProvider(githubOrgInstructionsProvider)); } } // Register skill provider for built-in agent customization skill if ('registerSkillProvider' in vscode.chat) { const agentCustomizationSkillProvider: vscode.ChatSkillProvider = instantiationService.createInstance(new SyncDescriptor(AgentCustomizationSkillProvider)); this._register(vscode.chat.registerSkillProvider(agentCustomizationSkillProvider)); // Enablement is controlled in core const troubleshootSkillProvider: vscode.ChatSkillProvider = instantiationService.createInstance(new SyncDescriptor(TroubleshootSkillProvider)); this._register(vscode.chat.registerSkillProvider(troubleshootSkillProvider)); } } } ================================================ FILE: src/extension/agents/vscode-node/skillFsProviderHelper.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; import { Emitter } from '../../../util/vs/base/common/event'; const SKILL_FILENAME = 'SKILL.md'; const SKILL_SCHEME = 'copilot-skill'; interface IDynamicSkillFolder { readonly folderName: string; readonly provideSkillContentBytes: () => Promise; } class SkillFsProvider implements vscode.FileSystemProvider { private readonly dynamicSkills = new Map(); private readonly _onDidChangeFile = new Emitter(); readonly onDidChangeFile = this._onDidChangeFile.event; constructor( private readonly extensionContext: IVSCodeExtensionContext, ) { } public registerDynamicSkill(dynamicSkill: IDynamicSkillFolder): vscode.Disposable { this.dynamicSkills.set(dynamicSkill.folderName, dynamicSkill); return { dispose: () => { this.dynamicSkills.delete(dynamicSkill.folderName); } }; } watch(_uri: vscode.Uri, _options: { readonly recursive: boolean; readonly excludes: readonly string[] }): vscode.Disposable { return { dispose: () => { } }; } async stat(uri: vscode.Uri): Promise { const parsed = this.parseSkillUri(uri); if (!parsed) { throw vscode.FileSystemError.FileNotFound(uri); } if (parsed.kind === 'root') { return { type: vscode.FileType.Directory, ctime: 0, mtime: 0, size: 0, }; } if (parsed.kind === 'folder') { if (!this.dynamicSkills.has(parsed.folderName)) { throw vscode.FileSystemError.FileNotFound(uri); } return { type: vscode.FileType.Directory, ctime: 0, mtime: 0, size: 0, }; } const dynamicSkill = this.dynamicSkills.get(parsed.folderName); if (!dynamicSkill) { throw vscode.FileSystemError.FileNotFound(uri); } if (parsed.relativePath === SKILL_FILENAME) { const content = await dynamicSkill.provideSkillContentBytes(); return { type: vscode.FileType.File, ctime: 0, mtime: Date.now(), size: content.length, }; } const assetUri = this.toAssetUri(parsed.folderName, parsed.relativePath); return vscode.workspace.fs.stat(assetUri); } async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> { const parsed = this.parseSkillUri(uri); if (!parsed) { throw vscode.FileSystemError.FileNotFound(uri); } if (parsed.kind === 'root') { return [...this.dynamicSkills.keys()].map(folderName => [folderName, vscode.FileType.Directory]); } if (parsed.kind === 'folder') { if (!this.dynamicSkills.has(parsed.folderName)) { throw vscode.FileSystemError.FileNotFound(uri); } const assetFolderUri = this.toAssetUri(parsed.folderName, ''); try { return await vscode.workspace.fs.readDirectory(assetFolderUri); } catch { return [[SKILL_FILENAME, vscode.FileType.File]]; } } const dynamicSkill = this.dynamicSkills.get(parsed.folderName); if (!dynamicSkill) { throw vscode.FileSystemError.FileNotFound(uri); } const assetFolderUri = this.toAssetUri(parsed.folderName, parsed.relativePath); return vscode.workspace.fs.readDirectory(assetFolderUri); } createDirectory(_uri: vscode.Uri): void { throw vscode.FileSystemError.NoPermissions('Readonly file system'); } async readFile(uri: vscode.Uri): Promise { const parsed = this.parseSkillUri(uri); if (!parsed || parsed.kind !== 'file') { throw vscode.FileSystemError.FileNotFound(uri); } const dynamicSkill = this.dynamicSkills.get(parsed.folderName); if (!dynamicSkill) { throw vscode.FileSystemError.FileNotFound(uri); } if (parsed.relativePath === SKILL_FILENAME) { return dynamicSkill.provideSkillContentBytes(); } const assetUri = this.toAssetUri(parsed.folderName, parsed.relativePath); return vscode.workspace.fs.readFile(assetUri); } writeFile(_uri: vscode.Uri, _content: Uint8Array, _options: { readonly create: boolean; readonly overwrite: boolean }): void { throw vscode.FileSystemError.NoPermissions('Readonly file system'); } delete(_uri: vscode.Uri, _options: { readonly recursive: boolean }): void { throw vscode.FileSystemError.NoPermissions('Readonly file system'); } rename(_oldUri: vscode.Uri, _newUri: vscode.Uri, _options: { readonly overwrite: boolean }): void { throw vscode.FileSystemError.NoPermissions('Readonly file system'); } private toAssetUri(folderName: string, relativePath: string): vscode.Uri { const segments = relativePath.split('/').filter(Boolean); return vscode.Uri.joinPath( this.extensionContext.extensionUri, 'assets', 'prompts', 'skills', folderName, ...segments, ); } private parseSkillUri(uri: vscode.Uri): | { kind: 'root' } | { kind: 'folder'; folderName: string } | { kind: 'file'; folderName: string; relativePath: string } | undefined { if (uri.scheme !== SKILL_SCHEME) { return undefined; } const segments = uri.path.split('/').filter(Boolean); if (segments.length === 0) { return { kind: 'root' }; } if (segments.length === 1) { return { kind: 'folder', folderName: segments[0] }; } return { kind: 'file', folderName: segments[0], relativePath: segments.slice(1).join('/'), }; } } let sharedFsProvider: | { provider: SkillFsProvider; registration: vscode.Disposable; } | undefined; function getOrCreateSharedFsProvider( extensionContext: IVSCodeExtensionContext, ): SkillFsProvider { if (!sharedFsProvider) { const provider = new SkillFsProvider(extensionContext); const registration = vscode.workspace.registerFileSystemProvider(SKILL_SCHEME, provider, { isReadonly: true }); sharedFsProvider = { provider, registration }; } return sharedFsProvider.provider; } export function registerDynamicSkillFolder( extensionContext: IVSCodeExtensionContext, folderName: string, provideSkillContentBytes: () => Promise, ): { readonly skillUri: vscode.Uri; readonly disposable: vscode.Disposable } { const provider = getOrCreateSharedFsProvider(extensionContext); const disposable = provider.registerDynamicSkill({ folderName, provideSkillContentBytes, }); return { skillUri: vscode.Uri.from({ scheme: SKILL_SCHEME, path: `/${folderName}/${SKILL_FILENAME}` }), disposable, }; } ================================================ FILE: src/extension/agents/vscode-node/test/askAgentProvider.spec.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { assert } from 'chai'; import * as os from 'os'; import * as path from 'path'; import { afterEach, beforeEach, suite, test } from 'vitest'; import * as vscode from 'vscode'; import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService'; import { InMemoryConfigurationService } from '../../../../platform/configuration/test/common/inMemoryConfigurationService'; import { IVSCodeExtensionContext } from '../../../../platform/extContext/common/extensionContext'; import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService'; import { MockExtensionContext } from '../../../../platform/test/node/extensionContext'; import { ITestingServicesAccessor } from '../../../../platform/test/node/services'; import { DisposableStore } from '../../../../util/vs/base/common/lifecycle'; import { SyncDescriptor } from '../../../../util/vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation'; import { createExtensionUnitTestingServices } from '../../../test/node/services'; import { AskAgentProvider } from '../askAgentProvider'; suite('AskAgentProvider', () => { let disposables: DisposableStore; let mockConfigurationService: InMemoryConfigurationService; let fileSystemService: IFileSystemService; let accessor: ITestingServicesAccessor; let instantiationService: IInstantiationService; beforeEach(() => { disposables = new DisposableStore(); const testingServiceCollection = createExtensionUnitTestingServices(disposables); const globalStoragePath = path.join(os.tmpdir(), 'ask-agent-test-' + Date.now()); testingServiceCollection.define(IVSCodeExtensionContext, new SyncDescriptor(MockExtensionContext, [globalStoragePath])); accessor = testingServiceCollection.createTestingAccessor(); disposables.add(accessor); instantiationService = accessor.get(IInstantiationService); mockConfigurationService = accessor.get(IConfigurationService) as InMemoryConfigurationService; fileSystemService = accessor.get(IFileSystemService); }); afterEach(() => { disposables.dispose(); }); function createProvider() { const provider = instantiationService.createInstance(AskAgentProvider); disposables.add(provider); return provider; } async function getAgentContent(agent: vscode.ChatResource): Promise { const content = await fileSystemService.readFile(agent.uri); return new TextDecoder().decode(content); } test('provideCustomAgents() returns an Ask agent with correct structure', async () => { const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); assert.ok(agents[0].uri, 'Agent should have a URI'); assert.ok(agents[0].uri.path.endsWith('.agent.md'), 'Agent URI should end with .agent.md'); }); test('returns agent content with base frontmatter when no settings configured', async () => { const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); // Should contain base read-only tools assert.ok(content.includes('search')); assert.ok(content.includes('read')); assert.ok(content.includes('web')); assert.ok(content.includes('github/issue_read')); // Should NOT contain editing tools assert.ok(!content.includes('\'edit'), 'Should not have edit or edit/... tools'); assert.ok(!content.includes('\'execute/run'), 'Should not have any execute/run... tool'); // Should have correct metadata assert.ok(content.includes('name: Ask')); assert.ok(content.includes('description: Answers questions without making changes')); }); test('merges additionalTools setting with base tools', async () => { await mockConfigurationService.setConfig(ConfigKey.AskAgentAdditionalTools, ['customTool1', 'customTool2']); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); // Should contain base tools assert.ok(content.includes('search')); assert.ok(content.includes('read')); // Should contain additional tools assert.ok(content.includes('customTool1')); assert.ok(content.includes('customTool2')); }); test('deduplicates tools when additionalTools overlaps with base tools', async () => { await mockConfigurationService.setConfig(ConfigKey.AskAgentAdditionalTools, ['search', 'newTool']); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); // Count occurrences of 'search' in tools list const toolsMatch = content.match(/tools: \[([^\]]+)\]/); assert.ok(toolsMatch, 'Tools list not found in agent content'); const toolsSection = toolsMatch[1]; const searchCount = (toolsSection.match(/'search'/g) || []).length; assert.equal(searchCount, 1, 'search tool should appear only once after deduplication'); // Should contain new tool assert.ok(content.includes('newTool')); }); test('applies model override from settings', async () => { await mockConfigurationService.setConfig(ConfigKey.AskAgentModel, 'Claude Haiku 4.5 (copilot)'); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); assert.ok(content.includes('model: Claude Haiku 4.5 (copilot)')); }); test('applies both additionalTools and model settings together', async () => { await mockConfigurationService.setConfig(ConfigKey.AskAgentAdditionalTools, ['extraTool']); await mockConfigurationService.setConfig(ConfigKey.AskAgentModel, 'claude-3-sonnet'); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); assert.ok(content.includes('extraTool')); assert.ok(content.includes('model: claude-3-sonnet')); }); test('fires onDidChangeCustomAgents when additionalTools setting changes', async () => { const provider = createProvider(); let eventFired = false; provider.onDidChangeCustomAgents(() => { eventFired = true; }); await mockConfigurationService.setConfig(ConfigKey.AskAgentAdditionalTools, ['newTool']); assert.equal(eventFired, true); }); test('fires onDidChangeCustomAgents when model setting changes', async () => { const provider = createProvider(); let eventFired = false; provider.onDidChangeCustomAgents(() => { eventFired = true; }); await mockConfigurationService.setConfig(ConfigKey.AskAgentModel, 'new-model'); assert.equal(eventFired, true); }); test('does not fire onDidChangeCustomAgents for unrelated setting changes', async () => { const provider = createProvider(); let eventFired = false; provider.onDidChangeCustomAgents(() => { eventFired = true; }); await mockConfigurationService.setConfig(ConfigKey.Advanced.FeedbackOnChange, true); assert.equal(eventFired, false); }); test('always includes askQuestions tool in generated content', async () => { const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); assert.ok(content.includes('vscode/askQuestions')); }); test('has correct label property', () => { const provider = createProvider(); assert.ok(provider.label.includes('Ask')); }); test('preserves body content after frontmatter when applying settings', async () => { await mockConfigurationService.setConfig(ConfigKey.AskAgentModel, 'test-model'); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); const content = await getAgentContent(agents[0]); assert.ok(content.includes('You are an ASK AGENT')); assert.ok(content.includes('NEVER modify files or run commands that change state')); }); test('handles empty additionalTools array gracefully', async () => { await mockConfigurationService.setConfig(ConfigKey.AskAgentAdditionalTools, []); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); // Should have base tools only assert.ok(content.includes('search')); assert.ok(content.includes('read')); }); test('handles empty model string gracefully', async () => { await mockConfigurationService.setConfig(ConfigKey.AskAgentModel, ''); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); assert.ok(!content.includes('model:')); }); test('does not include handoffs section', async () => { const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); const content = await getAgentContent(agents[0]); assert.ok(!content.includes('handoffs:'), 'Ask agent should not have handoffs'); }); test('body content instructs not to edit files', async () => { const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); const content = await getAgentContent(agents[0]); assert.ok(content.includes('NEVER modify files')); assert.ok(content.includes('NEVER use file editing tools')); }); }); ================================================ FILE: src/extension/agents/vscode-node/test/githubOrgChatResourcesService.spec.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { assert } from 'chai'; import { afterEach, beforeEach, suite, test } from 'vitest'; import type { ExtensionContext } from 'vscode'; import { AGENT_FILE_EXTENSION, INSTRUCTION_FILE_EXTENSION, PromptsType } from '../../../../platform/customInstructions/common/promptTypes'; import { FileType } from '../../../../platform/filesystem/common/fileTypes'; import { MockFileSystemService } from '../../../../platform/filesystem/node/test/mockFileSystemService'; import { MockAuthenticationService } from '../../../../platform/ignore/node/test/mockAuthenticationService'; import { MockGitService } from '../../../../platform/ignore/node/test/mockGitService'; import { MockWorkspaceService } from '../../../../platform/ignore/node/test/mockWorkspaceService'; import { ILogService } from '../../../../platform/log/common/logService'; import { DisposableStore } from '../../../../util/vs/base/common/lifecycle'; import { URI } from '../../../../util/vs/base/common/uri'; import { createExtensionUnitTestingServices } from '../../../test/node/services'; import { GitHubOrgChatResourcesService } from '../githubOrgChatResourcesService'; import { MockOctoKitService } from './mockOctoKitService'; suite('GitHubOrgChatResourcesService', () => { let disposables: DisposableStore; let mockExtensionContext: Partial; let mockFileSystem: MockFileSystemService; let mockGitService: MockGitService; let mockOctoKitService: MockOctoKitService; let mockWorkspaceService: MockWorkspaceService; let mockAuthService: MockAuthenticationService; let logService: ILogService; let service: GitHubOrgChatResourcesService; const storagePath = '/test/storage'; const storageUri = URI.file(storagePath); beforeEach(() => { disposables = new DisposableStore(); // Create a simple mock extension context with only globalStorageUri mockExtensionContext = { globalStorageUri: storageUri, }; mockFileSystem = new MockFileSystemService(); mockGitService = new MockGitService(); mockOctoKitService = new MockOctoKitService(); mockWorkspaceService = new MockWorkspaceService(); mockAuthService = new MockAuthenticationService(); // Set up testing services to get log service const testingServiceCollection = createExtensionUnitTestingServices(disposables); const accessor = disposables.add(testingServiceCollection.createTestingAccessor()); logService = accessor.get(ILogService); }); afterEach(() => { disposables.dispose(); mockOctoKitService?.reset(); }); function createService(): GitHubOrgChatResourcesService { service = new GitHubOrgChatResourcesService( mockAuthService as any, mockExtensionContext as any, mockFileSystem, mockGitService, logService, mockOctoKitService, mockWorkspaceService, ); disposables.add(service); return service; } suite('getPreferredOrganizationName', () => { test('returns organization from workspace repository when available', async () => { mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]); mockGitService.setRepositoryFetchUrls({ rootUri: URI.file('/workspace'), remoteFetchUrls: ['https://github.com/myorg/myrepo.git'] }); mockOctoKitService.setUserOrganizations(['myorg']); const service = createService(); const orgName = await service.getPreferredOrganizationName(); assert.equal(orgName, 'myorg'); }); test('returns organization from SSH URL format', async () => { mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]); mockGitService.setRepositoryFetchUrls({ rootUri: URI.file('/workspace'), remoteFetchUrls: ['git@github.com:sshorg/myrepo.git'] }); mockOctoKitService.setUserOrganizations(['sshorg']); const service = createService(); const orgName = await service.getPreferredOrganizationName(); assert.equal(orgName, 'sshorg'); }); test('falls back to user organizations when no workspace repo', async () => { mockWorkspaceService.setWorkspaceFolders([]); mockOctoKitService.setUserOrganizations(['fallbackorg', 'anotherorg']); const service = createService(); const orgName = await service.getPreferredOrganizationName(); assert.equal(orgName, 'fallbackorg'); }); test('falls back to user organizations when repo has no GitHub remote', async () => { mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]); mockGitService.setRepositoryFetchUrls({ rootUri: URI.file('/workspace'), remoteFetchUrls: ['https://gitlab.com/someorg/repo.git'] }); mockOctoKitService.setUserOrganizations(['fallbackorg']); const service = createService(); const orgName = await service.getPreferredOrganizationName(); assert.equal(orgName, 'fallbackorg'); }); test('returns undefined when user has no organizations', async () => { mockWorkspaceService.setWorkspaceFolders([]); mockOctoKitService.setUserOrganizations([]); const service = createService(); const orgName = await service.getPreferredOrganizationName(); assert.isUndefined(orgName); }); test('caches result on subsequent calls', async () => { mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]); mockGitService.setRepositoryFetchUrls({ rootUri: URI.file('/workspace'), remoteFetchUrls: ['https://github.com/cachedorg/repo.git'] }); mockOctoKitService.setUserOrganizations(['cachedorg']); const service = createService(); // First call const orgName1 = await service.getPreferredOrganizationName(); assert.equal(orgName1, 'cachedorg'); // Change the mock - should not affect cached result mockGitService.setRepositoryFetchUrls({ rootUri: URI.file('/workspace'), remoteFetchUrls: ['https://github.com/neworg/repo.git'] }); // Second call should return cached value const orgName2 = await service.getPreferredOrganizationName(); assert.equal(orgName2, 'cachedorg'); }); test('handles error in getUserOrganizations gracefully', async () => { mockWorkspaceService.setWorkspaceFolders([]); mockOctoKitService.getUserOrganizations = async () => { throw new Error('API Error'); }; const service = createService(); const orgName = await service.getPreferredOrganizationName(); assert.isUndefined(orgName); }); test('tries multiple remote URLs to find GitHub repo', async () => { mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]); mockGitService.setRepositoryFetchUrls({ rootUri: URI.file('/workspace'), remoteFetchUrls: [ 'https://gitlab.com/notgithub/repo.git', undefined as any, // Skip undefined 'https://github.com/foundorg/repo.git' ] }); mockOctoKitService.setUserOrganizations(['foundorg']); const service = createService(); const orgName = await service.getPreferredOrganizationName(); assert.equal(orgName, 'foundorg'); }); test('prefers Copilot sign-in org over arbitrary first org when no workspace repo', async () => { mockWorkspaceService.setWorkspaceFolders([]); mockOctoKitService.setUserOrganizations(['firstorg', 'copilotorg', 'thirdorg']); // Set Copilot token with organization_login_list indicating Copilot access through 'copilotorg' mockAuthService.copilotToken = { organizationLoginList: ['copilotorg'], } as any; const service = createService(); const orgName = await service.getPreferredOrganizationName(); assert.equal(orgName, 'copilotorg'); }); test('prefers workspace repo org over Copilot sign-in org', async () => { mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]); mockGitService.setRepositoryFetchUrls({ rootUri: URI.file('/workspace'), remoteFetchUrls: ['https://github.com/workspaceorg/repo.git'] }); mockOctoKitService.setUserOrganizations(['workspaceorg', 'copilotorg']); mockAuthService.copilotToken = { organizationLoginList: ['copilotorg'], } as any; const service = createService(); const orgName = await service.getPreferredOrganizationName(); assert.equal(orgName, 'workspaceorg'); }); test('uses Copilot org even when not in paginated user org list', async () => { mockWorkspaceService.setWorkspaceFolders([]); mockOctoKitService.setUserOrganizations(['firstorg', 'secondorg']); // Copilot org may not appear in paginated user org list but is still valid mockAuthService.copilotToken = { organizationLoginList: ['copilotorg'], } as any; const service = createService(); const orgName = await service.getPreferredOrganizationName(); // Copilot token orgs are trusted since they represent validated membership assert.equal(orgName, 'copilotorg'); }); test('falls back to first org when no Copilot token available', async () => { mockWorkspaceService.setWorkspaceFolders([]); mockOctoKitService.setUserOrganizations(['firstorg', 'secondorg']); mockAuthService.copilotToken = undefined; const service = createService(); const orgName = await service.getPreferredOrganizationName(); assert.equal(orgName, 'firstorg'); }); test('uses first matching Copilot org when multiple are available', async () => { mockWorkspaceService.setWorkspaceFolders([]); mockOctoKitService.setUserOrganizations(['thirdorg', 'secondcopilotorg', 'firstcopilotorg']); mockAuthService.copilotToken = { organizationLoginList: ['firstcopilotorg', 'secondcopilotorg'], } as any; const service = createService(); const orgName = await service.getPreferredOrganizationName(); // Should match 'firstcopilotorg' first in the copilot org list iteration assert.equal(orgName, 'firstcopilotorg'); }); }); suite.skip('startPolling', () => { test('invokes callback immediately with org name', async () => { mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]); mockGitService.setRepositoryFetchUrls({ rootUri: URI.file('/workspace'), remoteFetchUrls: ['https://github.com/pollingorg/repo.git'] }); mockOctoKitService.setUserOrganizations(['pollingorg']); const service = createService(); let capturedOrg: string | undefined; const subscription = service.startPolling(10000, async (orgName) => { capturedOrg = orgName; }); disposables.add(subscription); // Wait for initial poll await new Promise(resolve => setTimeout(resolve, 50)); assert.equal(capturedOrg, 'pollingorg'); }); test('does not invoke callback when no organization', async () => { mockWorkspaceService.setWorkspaceFolders([]); mockOctoKitService.setUserOrganizations([]); const service = createService(); let callbackInvoked = false; const subscription = service.startPolling(10000, async () => { callbackInvoked = true; }); disposables.add(subscription); await new Promise(resolve => setTimeout(resolve, 50)); assert.isFalse(callbackInvoked); }); test('stops polling when subscription is disposed', async () => { mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]); mockGitService.setRepositoryFetchUrls({ rootUri: URI.file('/workspace'), remoteFetchUrls: ['https://github.com/testorg/repo.git'] }); const service = createService(); let callCount = 0; const subscription = service.startPolling(50, async () => { callCount++; }); // Wait for initial poll await new Promise(resolve => setTimeout(resolve, 30)); const initialCount = callCount; // Dispose subscription subscription.dispose(); // Wait longer than poll interval await new Promise(resolve => setTimeout(resolve, 100)); // Call count should not have increased significantly after disposal assert.isAtMost(callCount - initialCount, 1); }); test('prevents concurrent polling', async () => { mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]); mockGitService.setRepositoryFetchUrls({ rootUri: URI.file('/workspace'), remoteFetchUrls: ['https://github.com/concurrent/repo.git'] }); mockOctoKitService.setUserOrganizations(['concurrent']); const service = createService(); let concurrentCalls = 0; let maxConcurrentCalls = 0; const subscription = service.startPolling(10, async () => { concurrentCalls++; maxConcurrentCalls = Math.max(maxConcurrentCalls, concurrentCalls); await new Promise(resolve => setTimeout(resolve, 50)); concurrentCalls--; }); disposables.add(subscription); // Wait for multiple poll cycles await new Promise(resolve => setTimeout(resolve, 100)); // Should never have more than 1 concurrent call assert.equal(maxConcurrentCalls, 1); }); test('handles callback errors gracefully', async () => { mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]); mockGitService.setRepositoryFetchUrls({ rootUri: URI.file('/workspace'), remoteFetchUrls: ['https://github.com/errororg/repo.git'] }); mockOctoKitService.setUserOrganizations(['errororg']); const service = createService(); let callCount = 0; const subscription = service.startPolling(30, async () => { callCount++; if (callCount === 1) { throw new Error('Callback error'); } }); disposables.add(subscription); // Wait for multiple poll cycles await new Promise(resolve => setTimeout(resolve, 100)); // Should continue polling even after error assert.isAtLeast(callCount, 2); }); }); suite('readCacheFile', () => { test('reads instruction file from cache', async () => { const cacheUri = URI.file(`${storagePath}/github/testorg/instructions/default${INSTRUCTION_FILE_EXTENSION}`); mockFileSystem.mockFile(cacheUri, '# Custom Instructions'); const service = createService(); const content = await service.readCacheFile(PromptsType.instructions, 'testorg', `default${INSTRUCTION_FILE_EXTENSION}`); assert.equal(content, '# Custom Instructions'); }); test('reads agent file from cache', async () => { const cacheUri = URI.file(`${storagePath}/github/testorg/agents/myagent${AGENT_FILE_EXTENSION}`); mockFileSystem.mockFile(cacheUri, '---\nname: My Agent\n---\nPrompt'); const service = createService(); const content = await service.readCacheFile(PromptsType.agent, 'testorg', `myagent${AGENT_FILE_EXTENSION}`); assert.equal(content, '---\nname: My Agent\n---\nPrompt'); }); test('returns undefined for missing file', async () => { const service = createService(); const content = await service.readCacheFile(PromptsType.instructions, 'testorg', 'nonexistent.instructions.md'); assert.isUndefined(content); }); test('sanitizes org name in path', async () => { // dash is preserved, uppercase becomes lowercase const cacheUri = URI.file(`${storagePath}/github/test-org/instructions/default${INSTRUCTION_FILE_EXTENSION}`); mockFileSystem.mockFile(cacheUri, 'Sanitized content'); const service = createService(); const content = await service.readCacheFile(PromptsType.instructions, 'Test-Org', `default${INSTRUCTION_FILE_EXTENSION}`); assert.equal(content, 'Sanitized content'); }); }); suite('writeCacheFile', () => { test('writes instruction file to cache', async () => { const service = createService(); const result = await service.writeCacheFile( PromptsType.instructions, 'testorg', `default${INSTRUCTION_FILE_EXTENSION}`, '# New Instructions' ); assert.isTrue(result); // Verify file was written const cacheUri = URI.file(`${storagePath}/github/testorg/instructions/default${INSTRUCTION_FILE_EXTENSION}`); const content = await mockFileSystem.readFile(cacheUri); assert.equal(new TextDecoder().decode(content), '# New Instructions'); }); test('writes agent file to cache', async () => { const service = createService(); const result = await service.writeCacheFile( PromptsType.agent, 'testorg', `myagent${AGENT_FILE_EXTENSION}`, '---\nname: Agent\n---\nPrompt' ); assert.isTrue(result); const cacheUri = URI.file(`${storagePath}/github/testorg/agents/myagent${AGENT_FILE_EXTENSION}`); const content = await mockFileSystem.readFile(cacheUri); assert.equal(new TextDecoder().decode(content), '---\nname: Agent\n---\nPrompt'); }); test('returns false when content unchanged with checkForChanges', async () => { const cacheUri = URI.file(`${storagePath}/github/testorg/instructions/default${INSTRUCTION_FILE_EXTENSION}`); mockFileSystem.mockFile(cacheUri, 'Same content'); const service = createService(); const result = await service.writeCacheFile( PromptsType.instructions, 'testorg', `default${INSTRUCTION_FILE_EXTENSION}`, 'Same content', { checkForChanges: true } ); assert.isFalse(result); }); test('returns true when content changed with checkForChanges', async () => { const cacheUri = URI.file(`${storagePath}/github/testorg/instructions/default${INSTRUCTION_FILE_EXTENSION}`); mockFileSystem.mockFile(cacheUri, 'Old content'); const service = createService(); const result = await service.writeCacheFile( PromptsType.instructions, 'testorg', `default${INSTRUCTION_FILE_EXTENSION}`, 'New content', { checkForChanges: true } ); assert.isTrue(result); }); test('returns true when file does not exist with checkForChanges', async () => { const service = createService(); const result = await service.writeCacheFile( PromptsType.instructions, 'neworg', `default${INSTRUCTION_FILE_EXTENSION}`, 'Content', { checkForChanges: true } ); assert.isTrue(result); }); test('returns true when file size differs with checkForChanges', async () => { const cacheUri = URI.file(`${storagePath}/github/testorg/instructions/default${INSTRUCTION_FILE_EXTENSION}`); mockFileSystem.mockFile(cacheUri, 'Short'); const service = createService(); const result = await service.writeCacheFile( PromptsType.instructions, 'testorg', `default${INSTRUCTION_FILE_EXTENSION}`, 'Much longer content that differs in size', { checkForChanges: true } ); assert.isTrue(result); }); test('creates directory structure if not exists', async () => { const service = createService(); await service.writeCacheFile( PromptsType.agent, 'neworg', `agent${AGENT_FILE_EXTENSION}`, 'Content' ); const cacheUri = URI.file(`${storagePath}/github/neworg/agents/agent${AGENT_FILE_EXTENSION}`); const content = await mockFileSystem.readFile(cacheUri); assert.equal(new TextDecoder().decode(content), 'Content'); }); test('sanitizes org name before writing', async () => { const service = createService(); await service.writeCacheFile( PromptsType.instructions, 'My-Org!@#', `default${INSTRUCTION_FILE_EXTENSION}`, 'Content' ); // dash is preserved, special chars become underscore, uppercase becomes lowercase const cacheUri = URI.file(`${storagePath}/github/my-org___/instructions/default${INSTRUCTION_FILE_EXTENSION}`); const content = await mockFileSystem.readFile(cacheUri); assert.equal(new TextDecoder().decode(content), 'Content'); }); }); suite('clearCache', () => { test('deletes all instruction files for organization', async () => { const cacheDir = URI.file(`${storagePath}/github/testorg/instructions`); mockFileSystem.mockDirectory(cacheDir, [ [`file1${INSTRUCTION_FILE_EXTENSION}`, FileType.File], [`file2${INSTRUCTION_FILE_EXTENSION}`, FileType.File], ]); mockFileSystem.mockFile(URI.joinPath(cacheDir, `file1${INSTRUCTION_FILE_EXTENSION}`), 'Content 1'); mockFileSystem.mockFile(URI.joinPath(cacheDir, `file2${INSTRUCTION_FILE_EXTENSION}`), 'Content 2'); const service = createService(); await service.clearCache(PromptsType.instructions, 'testorg'); // Files should be deleted let file1Exists = true; let file2Exists = true; try { await mockFileSystem.readFile(URI.joinPath(cacheDir, `file1${INSTRUCTION_FILE_EXTENSION}`)); } catch { file1Exists = false; } try { await mockFileSystem.readFile(URI.joinPath(cacheDir, `file2${INSTRUCTION_FILE_EXTENSION}`)); } catch { file2Exists = false; } assert.isFalse(file1Exists); assert.isFalse(file2Exists); }); test('excludes specified files from deletion', async () => { const cacheDir = URI.file(`${storagePath}/github/testorg/instructions`); mockFileSystem.mockDirectory(cacheDir, [ [`keep${INSTRUCTION_FILE_EXTENSION}`, FileType.File], [`delete${INSTRUCTION_FILE_EXTENSION}`, FileType.File], ]); mockFileSystem.mockFile(URI.joinPath(cacheDir, `keep${INSTRUCTION_FILE_EXTENSION}`), 'Keep this'); mockFileSystem.mockFile(URI.joinPath(cacheDir, `delete${INSTRUCTION_FILE_EXTENSION}`), 'Delete this'); const service = createService(); await service.clearCache(PromptsType.instructions, 'testorg', new Set([`keep${INSTRUCTION_FILE_EXTENSION}`])); // Kept file should still exist const keepContent = await mockFileSystem.readFile(URI.joinPath(cacheDir, `keep${INSTRUCTION_FILE_EXTENSION}`)); assert.equal(new TextDecoder().decode(keepContent), 'Keep this'); // Deleted file should not exist let deleteExists = true; try { await mockFileSystem.readFile(URI.joinPath(cacheDir, `delete${INSTRUCTION_FILE_EXTENSION}`)); } catch { deleteExists = false; } assert.isFalse(deleteExists); }); test('skips non-matching file extensions', async () => { const cacheDir = URI.file(`${storagePath}/github/testorg/instructions`); mockFileSystem.mockDirectory(cacheDir, [ [`valid${INSTRUCTION_FILE_EXTENSION}`, FileType.File], ['invalid.txt', FileType.File], ]); mockFileSystem.mockFile(URI.joinPath(cacheDir, `valid${INSTRUCTION_FILE_EXTENSION}`), 'Valid'); mockFileSystem.mockFile(URI.joinPath(cacheDir, 'invalid.txt'), 'Invalid'); const service = createService(); await service.clearCache(PromptsType.instructions, 'testorg'); // Valid file should be deleted let validExists = true; try { await mockFileSystem.readFile(URI.joinPath(cacheDir, `valid${INSTRUCTION_FILE_EXTENSION}`)); } catch { validExists = false; } assert.isFalse(validExists); // Invalid file should still exist const invalidContent = await mockFileSystem.readFile(URI.joinPath(cacheDir, 'invalid.txt')); assert.equal(new TextDecoder().decode(invalidContent), 'Invalid'); }); test('handles non-existent cache directory gracefully', async () => { const service = createService(); // Should not throw await service.clearCache(PromptsType.instructions, 'nonexistentorg'); }); test('skips directories in cache folder', async () => { const cacheDir = URI.file(`${storagePath}/github/testorg/instructions`); mockFileSystem.mockDirectory(cacheDir, [ [`file${INSTRUCTION_FILE_EXTENSION}`, FileType.File], ['subfolder', FileType.Directory], ]); mockFileSystem.mockFile(URI.joinPath(cacheDir, `file${INSTRUCTION_FILE_EXTENSION}`), 'Content'); mockFileSystem.mockDirectory(URI.joinPath(cacheDir, 'subfolder'), []); const service = createService(); await service.clearCache(PromptsType.instructions, 'testorg'); // Directory should still exist const dirStat = await mockFileSystem.stat(URI.joinPath(cacheDir, 'subfolder')); assert.ok(dirStat); }); }); suite('listCachedFiles', () => { test('lists all instruction files for organization', async () => { const cacheDir = URI.file(`${storagePath}/github/testorg/instructions`); mockFileSystem.mockDirectory(cacheDir, [ [`file1${INSTRUCTION_FILE_EXTENSION}`, FileType.File], [`file2${INSTRUCTION_FILE_EXTENSION}`, FileType.File], ]); const service = createService(); const files = await service.listCachedFiles(PromptsType.instructions, 'testorg'); assert.equal(files.length, 2); const fileNames = files.map(f => f.uri.path.split('/').pop()); assert.include(fileNames, `file1${INSTRUCTION_FILE_EXTENSION}`); assert.include(fileNames, `file2${INSTRUCTION_FILE_EXTENSION}`); }); test('lists all agent files for organization', async () => { const cacheDir = URI.file(`${storagePath}/github/testorg/agents`); mockFileSystem.mockDirectory(cacheDir, [ [`agent1${AGENT_FILE_EXTENSION}`, FileType.File], [`agent2${AGENT_FILE_EXTENSION}`, FileType.File], ]); const service = createService(); const files = await service.listCachedFiles(PromptsType.agent, 'testorg'); assert.equal(files.length, 2); const fileNames = files.map(f => f.uri.path.split('/').pop()); assert.include(fileNames, `agent1${AGENT_FILE_EXTENSION}`); assert.include(fileNames, `agent2${AGENT_FILE_EXTENSION}`); }); test('returns empty array for non-existent directory', async () => { const service = createService(); const files = await service.listCachedFiles(PromptsType.instructions, 'nonexistent'); assert.deepEqual(files, []); }); test('filters out non-matching file extensions', async () => { const cacheDir = URI.file(`${storagePath}/github/testorg/instructions`); mockFileSystem.mockDirectory(cacheDir, [ [`valid${INSTRUCTION_FILE_EXTENSION}`, FileType.File], ['invalid.txt', FileType.File], ['readme.md', FileType.File], ]); const service = createService(); const files = await service.listCachedFiles(PromptsType.instructions, 'testorg'); assert.equal(files.length, 1); assert.ok(files[0].uri.path.endsWith(INSTRUCTION_FILE_EXTENSION)); }); test('filters out directories', async () => { const cacheDir = URI.file(`${storagePath}/github/testorg/agents`); mockFileSystem.mockDirectory(cacheDir, [ [`agent${AGENT_FILE_EXTENSION}`, FileType.File], ['subfolder', FileType.Directory], ]); const service = createService(); const files = await service.listCachedFiles(PromptsType.agent, 'testorg'); assert.equal(files.length, 1); assert.ok(files[0].uri.path.endsWith(AGENT_FILE_EXTENSION)); }); test('returns correct URI structure for files', async () => { const cacheDir = URI.file(`${storagePath}/github/myorg/instructions`); mockFileSystem.mockDirectory(cacheDir, [ [`custom${INSTRUCTION_FILE_EXTENSION}`, FileType.File], ]); const service = createService(); const files = await service.listCachedFiles(PromptsType.instructions, 'myorg'); assert.equal(files.length, 1); assert.ok(files[0].uri.path.includes('/github/')); assert.ok(files[0].uri.path.includes('/myorg/')); assert.ok(files[0].uri.path.includes('/instructions/')); }); }); suite('workspace folder change handling', () => { test('invalidates org cache when workspace folders change', async () => { mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace1')]); mockGitService.setRepositoryFetchUrls({ rootUri: URI.file('/workspace1'), remoteFetchUrls: ['https://github.com/org1/repo.git'] }); mockOctoKitService.setUserOrganizations(['org1', 'org2']); const service = createService(); // Get initial org name const orgName1 = await service.getPreferredOrganizationName(); assert.equal(orgName1, 'org1'); // Simulate workspace folder change by updating mocks mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace2')]); mockGitService.setRepositoryFetchUrls({ rootUri: URI.file('/workspace2'), remoteFetchUrls: ['https://github.com/org2/repo.git'] }); // The cache should be cleared on workspace change event // Since we can't easily fire the event, we verify the subscription is set up // by checking that disposal works service.dispose(); }); }); suite('getCacheSubdirectory helper', () => { test('uses instructions subdirectory for instructions type', async () => { const service = createService(); await service.writeCacheFile( PromptsType.instructions, 'testorg', `file${INSTRUCTION_FILE_EXTENSION}`, 'Content' ); const files = await service.listCachedFiles(PromptsType.instructions, 'testorg'); assert.ok(files[0].uri.path.includes('/instructions/')); }); test('uses agents subdirectory for agent type', async () => { const service = createService(); await service.writeCacheFile( PromptsType.agent, 'testorg', `file${AGENT_FILE_EXTENSION}`, 'Content' ); const files = await service.listCachedFiles(PromptsType.agent, 'testorg'); assert.ok(files[0].uri.path.includes('/agents/')); }); }); suite('file validation', () => { test('validates instruction file extension', async () => { const cacheDir = URI.file(`${storagePath}/github/testorg/instructions`); mockFileSystem.mockDirectory(cacheDir, [ [`valid${INSTRUCTION_FILE_EXTENSION}`, FileType.File], ['valid.agent.md', FileType.File], // Wrong extension for instructions ]); const service = createService(); const files = await service.listCachedFiles(PromptsType.instructions, 'testorg'); assert.equal(files.length, 1); assert.ok(files[0].uri.path.endsWith(INSTRUCTION_FILE_EXTENSION)); }); test('validates agent file extension', async () => { const cacheDir = URI.file(`${storagePath}/github/testorg/agents`); mockFileSystem.mockDirectory(cacheDir, [ [`valid${AGENT_FILE_EXTENSION}`, FileType.File], ['valid.instructions.md', FileType.File], // Wrong extension for agents ]); const service = createService(); const files = await service.listCachedFiles(PromptsType.agent, 'testorg'); assert.equal(files.length, 1); assert.ok(files[0].uri.path.endsWith(AGENT_FILE_EXTENSION)); }); }); }); ================================================ FILE: src/extension/agents/vscode-node/test/githubOrgCustomAgentProvider.spec.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { assert } from 'chai'; import { afterEach, beforeEach, suite, test, vi } from 'vitest'; import type { ExtensionContext } from 'vscode'; import { Scalar } from 'yaml'; import { PromptsType } from '../../../../platform/customInstructions/common/promptTypes'; import { MockFileSystemService } from '../../../../platform/filesystem/node/test/mockFileSystemService'; import { CustomAgentDetails, CustomAgentListItem, CustomAgentListOptions } from '../../../../platform/github/common/githubService'; import { MockAuthenticationService } from '../../../../platform/ignore/node/test/mockAuthenticationService'; import { MockGitService } from '../../../../platform/ignore/node/test/mockGitService'; import { MockWorkspaceService } from '../../../../platform/ignore/node/test/mockWorkspaceService'; import { ILogService } from '../../../../platform/log/common/logService'; import { DisposableStore } from '../../../../util/vs/base/common/lifecycle'; import { URI } from '../../../../util/vs/base/common/uri'; import { parse } from '../../../../util/vs/base/common/yaml'; import { createExtensionUnitTestingServices } from '../../../test/node/services'; import { GitHubOrgChatResourcesService } from '../githubOrgChatResourcesService'; import { GitHubOrgCustomAgentProvider, looksLikeNumber, yamlString } from '../githubOrgCustomAgentProvider'; import { MockOctoKitService } from './mockOctoKitService'; suite('GitHubOrgCustomAgentProvider', () => { let disposables: DisposableStore; let mockOctoKitService: MockOctoKitService; let mockFileSystem: MockFileSystemService; let mockGitService: MockGitService; let mockWorkspaceService: MockWorkspaceService; let mockExtensionContext: Partial; let mockAuthService: MockAuthenticationService; let accessor: any; let provider: GitHubOrgCustomAgentProvider; let resourcesService: GitHubOrgChatResourcesService; const storagePath = '/tmp/test-storage'; const storageUri = URI.file(storagePath); beforeEach(() => { vi.useFakeTimers(); disposables = new DisposableStore(); // Create mocks for real GitHubOrgChatResourcesService mockOctoKitService = new MockOctoKitService(); mockFileSystem = new MockFileSystemService(); mockGitService = new MockGitService(); mockWorkspaceService = new MockWorkspaceService(); mockExtensionContext = { globalStorageUri: storageUri, }; mockAuthService = new MockAuthenticationService(); // Default: user is in 'testorg' and workspace belongs to 'testorg' mockOctoKitService.setUserOrganizations(['testorg']); mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]); mockGitService.setRepositoryFetchUrls({ rootUri: URI.file('/workspace'), remoteFetchUrls: ['https://github.com/testorg/repo.git'] }); // Set up testing services const testingServiceCollection = createExtensionUnitTestingServices(disposables); accessor = disposables.add(testingServiceCollection.createTestingAccessor()); }); afterEach(() => { vi.useRealTimers(); disposables.dispose(); mockOctoKitService.clearAgents(); }); function createProvider() { // Create the real GitHubOrgChatResourcesService with mocked dependencies resourcesService = new GitHubOrgChatResourcesService( mockAuthService as any, mockExtensionContext as any, mockFileSystem, mockGitService, accessor.get(ILogService), mockOctoKitService, mockWorkspaceService, ); disposables.add(resourcesService); // Create provider with real resources service provider = new GitHubOrgCustomAgentProvider( mockOctoKitService, accessor.get(ILogService), resourcesService, ); disposables.add(provider); return provider; } /** * Advance timers and wait for polling callback to complete. * Uses a small time advance to trigger the initial poll without infinite loops. */ async function waitForPolling(): Promise { // Advance just enough to let initial poll complete, but not trigger interval polls await vi.advanceTimersByTimeAsync(10); } /** * Helper to pre-populate cache files in mock filesystem. */ function prepopulateCache(orgName: string, files: Map): void { const cacheDir = URI.file(`${storagePath}/github/${orgName}/agents`); const dirEntries: [string, import('../../../../platform/filesystem/common/fileTypes').FileType][] = []; for (const [filename, content] of files) { mockFileSystem.mockFile(URI.joinPath(cacheDir, filename), content); dirEntries.push([filename, 1 /* FileType.File */]); } mockFileSystem.mockDirectory(cacheDir, dirEntries); } test('returns empty array when user has no organizations', async () => { mockOctoKitService.setUserOrganizations([]); mockWorkspaceService.setWorkspaceFolders([]); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.deepEqual(agents, []); }); test('returns empty array when no organizations and no cached files', async () => { // With no organizations and no cached files, should return empty mockOctoKitService.setUserOrganizations([]); mockWorkspaceService.setWorkspaceFolders([]); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.deepEqual(agents, []); }); // todo: MockFileSystemService previously had a bug where deleted files would // still show up when listing directories. This was fixed and caused this test // to fail: test_agent.md is cleared from the cache in the first poll test.skip('returns cached agents on first call', async () => { // Set up file system mocks BEFORE creating provider to avoid race with background fetch // Also prevent background fetch from interfering by having no organizations mockOctoKitService.setUserOrganizations([]); mockWorkspaceService.setWorkspaceFolders([]); // Pre-populate cache with org folder (but keep testorg folder structure) const agentContent = `--- name: Test Agent description: A test agent --- Test prompt content`; prepopulateCache('testorg', new Map([['test_agent.agent.md', agentContent]])); // Re-enable testorg for cache reading (user is in org, but no workspace repo) mockOctoKitService.setUserOrganizations(['testorg']); const provider = createProvider(); // Wait for initial poll attempt (won't fetch since no agents in API) await waitForPolling(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const agentName = agents[0].uri.path.split('/').pop()?.replace('.agent.md', ''); assert.equal(agentName, 'test_agent'); }); test('fetches and caches agents from API', async () => { // Mock API response BEFORE creating provider const mockAgent: CustomAgentListItem = { name: 'api_agent', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 1, repo_name: 'testrepo', display_name: 'API Agent', description: 'An agent from API', tools: ['tool1'], version: 'v1', }; mockOctoKitService.setCustomAgents([mockAgent]); const mockDetails: CustomAgentDetails = { ...mockAgent, prompt: 'API prompt content', }; mockOctoKitService.setAgentDetails('api_agent', mockDetails); const provider = createProvider(); // Wait for background fetch to complete await waitForPolling(); // Second call should return newly cached agents from memory const agents2 = await provider.provideCustomAgents({}, {} as any); assert.equal(agents2.length, 1); const agentName2 = agents2[0].uri.path.split('/').pop()?.replace('.agent.md', ''); assert.equal(agentName2, 'api_agent'); // Third call should also return from memory cache without file I/O const agents3 = await provider.provideCustomAgents({}, {} as any); assert.equal(agents3.length, 1); const agentName3 = agents3[0].uri.path.split('/').pop()?.replace('.agent.md', ''); assert.equal(agentName3, 'api_agent'); }); test('generates correct markdown format for agents', async () => { const provider = createProvider(); const mockAgent: CustomAgentListItem = { name: 'full_agent', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 1, repo_name: 'testrepo', display_name: 'Full Agent', description: 'A fully configured agent', tools: ['tool1', 'tool2'], version: 'v1', argument_hint: 'Provide context', target: 'vscode', }; mockOctoKitService.setCustomAgents([mockAgent]); const mockDetails: CustomAgentDetails = { ...mockAgent, prompt: 'Detailed prompt content', model: 'gpt-4', disable_model_invocation: true, }; mockOctoKitService.setAgentDetails('full_agent', mockDetails); await provider.provideCustomAgents({}, {} as any); await waitForPolling(); // Check cached file content using the real service const content = await resourcesService.readCacheFile(PromptsType.agent, 'testorg', 'full_agent.agent.md'); const expectedContent = `--- name: Full Agent description: A fully configured agent tools: - tool1 - tool2 argument-hint: Provide context target: vscode model: gpt-4 disable-model-invocation: true --- Detailed prompt content `; assert.equal(content, expectedContent); }); test('generates markdown with user-invocable property', async () => { const provider = createProvider(); const mockAgent: CustomAgentListItem = { name: 'invocable_agent', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 1, repo_name: 'testrepo', display_name: 'Invocable Agent', description: 'An agent with user-invocable set', tools: [], version: 'v1', }; mockOctoKitService.setCustomAgents([mockAgent]); const mockDetails: CustomAgentDetails = { ...mockAgent, prompt: 'Invocable prompt content', user_invocable: true, }; mockOctoKitService.setAgentDetails('invocable_agent', mockDetails); await provider.provideCustomAgents({}, {} as any); await waitForPolling(); const content = await resourcesService.readCacheFile(PromptsType.agent, 'testorg', 'invocable_agent.agent.md'); const expectedContent = `--- name: Invocable Agent description: An agent with user-invocable set user-invocable: true --- Invocable prompt content `; assert.equal(content, expectedContent); }); test('generates markdown with false values for disable-model-invocation and user-invocable', async () => { const provider = createProvider(); const mockAgent: CustomAgentListItem = { name: 'false_flags_agent', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 1, repo_name: 'testrepo', display_name: 'False Flags Agent', description: 'Agent with false boolean flags', tools: [], version: 'v1', }; mockOctoKitService.setCustomAgents([mockAgent]); const mockDetails: CustomAgentDetails = { ...mockAgent, prompt: 'False flags prompt', disable_model_invocation: false, user_invocable: false, }; mockOctoKitService.setAgentDetails('false_flags_agent', mockDetails); await provider.provideCustomAgents({}, {} as any); await waitForPolling(); const content = await resourcesService.readCacheFile(PromptsType.agent, 'testorg', 'false_flags_agent.agent.md'); const expectedContent = `--- name: False Flags Agent description: Agent with false boolean flags disable-model-invocation: false user-invocable: false --- False flags prompt `; assert.equal(content, expectedContent); }); test('preserves agent name in filename', async () => { // Note: The provider does NOT sanitize filenames - it uses the agent name directly. // This test documents the actual behavior. const provider = createProvider(); const mockAgent: CustomAgentListItem = { name: 'my-agent_name', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 1, repo_name: 'testrepo', display_name: 'My Agent', description: 'Test filename', tools: [], version: 'v1', }; mockOctoKitService.setCustomAgents([mockAgent]); const mockDetails: CustomAgentDetails = { ...mockAgent, prompt: 'Prompt content', }; mockOctoKitService.setAgentDetails('my-agent_name', mockDetails); await provider.provideCustomAgents({}, {} as any); await waitForPolling(); // File is created with the exact agent name (no sanitization) const content = await resourcesService.readCacheFile(PromptsType.agent, 'testorg', 'my-agent_name.agent.md'); assert.ok(content, 'File should exist with agent name as filename'); }); test.skip('fires change event when cache is updated on first fetch', async () => { const provider = createProvider(); const mockAgent: CustomAgentListItem = { name: 'changing_agent', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 1, repo_name: 'testrepo', display_name: 'Changing Agent', description: 'Will change', tools: [], version: 'v1', }; mockOctoKitService.setCustomAgents([mockAgent]); const mockDetails: CustomAgentDetails = { ...mockAgent, prompt: 'Initial prompt', }; mockOctoKitService.setAgentDetails('changing_agent', mockDetails); let eventFired = false; provider.onDidChangeCustomAgents(() => { eventFired = true; }); // First call triggers background fetch await provider.provideCustomAgents({}, {} as any); await waitForPolling(); // Event should fire after initial successful fetch assert.equal(eventFired, true); }); test('handles API errors gracefully', async () => { const provider = createProvider(); // Make the API throw an error mockOctoKitService.getCustomAgents = async () => { throw new Error('API Error'); }; // Should not throw, should return empty array const agents = await provider.provideCustomAgents({}, {} as any); assert.deepEqual(agents, []); }); test('passes query options to API correctly', async () => { const provider = createProvider(); let capturedOptions: CustomAgentListOptions | undefined; mockOctoKitService.getCustomAgents = async (owner: string, repo: string, options?: CustomAgentListOptions) => { capturedOptions = options; return []; }; await provider.provideCustomAgents({}, {} as any); await waitForPolling(); assert.ok(capturedOptions); assert.deepEqual(capturedOptions.includeSources, ['org', 'enterprise']); }); test('prevents concurrent fetches when called multiple times rapidly', async () => { const provider = createProvider(); let apiCallCount = 0; mockOctoKitService.getCustomAgents = async () => { apiCallCount++; // Simulate slow API call - use real timer for this await new Promise(resolve => { const realSetTimeout = globalThis.setTimeout; realSetTimeout(resolve, 50); }); return []; }; // Make multiple concurrent calls const promise1 = provider.provideCustomAgents({}, {} as any); const promise2 = provider.provideCustomAgents({}, {} as any); const promise3 = provider.provideCustomAgents({}, {} as any); await Promise.all([promise1, promise2, promise3]); await waitForPolling(); // API should only be called once due to isFetching guard assert.equal(apiCallCount, 1); }); test('handles partial agent detail fetch failures gracefully', async () => { const agents: CustomAgentListItem[] = [ { name: 'agent1', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 1, repo_name: 'testrepo', display_name: 'Agent 1', description: 'First agent', tools: [], version: 'v1', }, { name: 'agent2', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 1, repo_name: 'testrepo', display_name: 'Agent 2', description: 'Second agent', tools: [], version: 'v1', }, ]; mockOctoKitService.setCustomAgents(agents); // Set details for only the first agent (second will fail) mockOctoKitService.setAgentDetails('agent1', { ...agents[0], prompt: 'Agent 1 prompt', }); // Pre-populate file cache with the first agent to simulate previous successful state const agentContent = `--- name: Agent 1 description: First agent --- Agent 1 prompt`; prepopulateCache('testorg', new Map([['agent1.agent.md', agentContent]])); const provider = createProvider(); await waitForPolling(); // With error handling, partial failures skip cache update for that org // So the existing file cache is returned with the one successful agent const cachedAgents = await provider.provideCustomAgents({}, {} as any); assert.equal(cachedAgents.length, 1); const cachedAgentName = cachedAgents[0].uri.path.split('/').pop()?.replace('.agent.md', ''); assert.equal(cachedAgentName, 'agent1'); }); test('caches agents in memory after first successful fetch', async () => { // Initial setup with one agent BEFORE creating provider const initialAgent: CustomAgentListItem = { name: 'initial_agent', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 1, repo_name: 'testrepo', display_name: 'Initial Agent', description: 'First agent', tools: [], version: 'v1', }; mockOctoKitService.setCustomAgents([initialAgent]); mockOctoKitService.setAgentDetails('initial_agent', { ...initialAgent, prompt: 'Initial prompt', }); const provider = createProvider(); await waitForPolling(); // After successful fetch, subsequent calls return from memory const agents1 = await provider.provideCustomAgents({}, {} as any); assert.equal(agents1.length, 1); const agentName1 = agents1[0].uri.path.split('/').pop()?.replace('.agent.md', ''); assert.equal(agentName1, 'initial_agent'); // Even if API is updated, memory cache is used const newAgent: CustomAgentListItem = { name: 'new_agent', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 1, repo_name: 'testrepo', display_name: 'New Agent', description: 'Newly added agent', tools: [], version: 'v1', }; mockOctoKitService.setCustomAgents([initialAgent, newAgent]); mockOctoKitService.setAgentDetails('new_agent', { ...newAgent, prompt: 'New prompt', }); // Memory cache returns old results without refetching const agents2 = await provider.provideCustomAgents({}, {} as any); assert.equal(agents2.length, 1); const agentName2ForMemory = agents2[0].uri.path.split('/').pop()?.replace('.agent.md', ''); assert.equal(agentName2ForMemory, 'initial_agent'); }); test('memory cache persists after first successful fetch', async () => { // Initial setup with two agents BEFORE creating provider const agents: CustomAgentListItem[] = [ { name: 'agent1', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 1, repo_name: 'testrepo', display_name: 'Agent 1', description: 'First agent', tools: [], version: 'v1', }, { name: 'agent2', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 1, repo_name: 'testrepo', display_name: 'Agent 2', description: 'Second agent', tools: [], version: 'v1', }, ]; mockOctoKitService.setCustomAgents(agents); mockOctoKitService.setAgentDetails('agent1', { ...agents[0], prompt: 'Prompt 1' }); mockOctoKitService.setAgentDetails('agent2', { ...agents[1], prompt: 'Prompt 2' }); const provider = createProvider(); await waitForPolling(); // Verify both agents are cached const cachedAgents1 = await provider.provideCustomAgents({}, {} as any); assert.equal(cachedAgents1.length, 2); // Remove one agent from API mockOctoKitService.setCustomAgents([agents[0]]); // Memory cache still returns both agents (no refetch) const cachedAgents2 = await provider.provideCustomAgents({}, {} as any); assert.equal(cachedAgents2.length, 2); const cachedAgent2Name1 = cachedAgents2[0].uri.path.split('/').pop()?.replace('.agent.md', ''); const cachedAgent2Name2 = cachedAgents2[1].uri.path.split('/').pop()?.replace('.agent.md', ''); assert.equal(cachedAgent2Name1, 'agent1'); assert.equal(cachedAgent2Name2, 'agent2'); }); test.skip('does not fire change event when content is identical', async () => { const provider = createProvider(); const mockAgent: CustomAgentListItem = { name: 'stable_agent', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 1, repo_name: 'testrepo', display_name: 'Stable Agent', description: 'Unchanging agent', tools: [], version: 'v1', }; mockOctoKitService.setCustomAgents([mockAgent]); mockOctoKitService.setAgentDetails('stable_agent', { ...mockAgent, prompt: 'Stable prompt', }); await provider.provideCustomAgents({}, {} as any); await waitForPolling(); let changeEventCount = 0; provider.onDidChangeCustomAgents(() => { changeEventCount++; }); // Fetch again with identical content await provider.provideCustomAgents({}, {} as any); await waitForPolling(); // No change event should fire assert.equal(changeEventCount, 0); }); test('memory cache persists even when API returns empty list', async () => { // Setup with initial agents BEFORE creating provider const mockAgent: CustomAgentListItem = { name: 'temporary_agent', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 1, repo_name: 'testrepo', display_name: 'Temporary Agent', description: 'Will be removed', tools: [], version: 'v1', }; mockOctoKitService.setCustomAgents([mockAgent]); mockOctoKitService.setAgentDetails('temporary_agent', { ...mockAgent, prompt: 'Temporary prompt', }); const provider = createProvider(); await waitForPolling(); // Verify agent is cached const agents1 = await provider.provideCustomAgents({}, {} as any); assert.equal(agents1.length, 1); // API now returns empty array mockOctoKitService.setCustomAgents([]); // Memory cache still returns the agent (no refetch) const agents2 = await provider.provideCustomAgents({}, {} as any); assert.equal(agents2.length, 1); const temporaryAgentName = agents2[0].uri.path.split('/').pop()?.replace('.agent.md', ''); assert.equal(temporaryAgentName, 'temporary_agent'); }); test('generates markdown with only required fields', async () => { const provider = createProvider(); // Agent with minimal fields (no optional fields) const mockAgent: CustomAgentListItem = { name: 'minimal_agent', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 1, repo_name: 'testrepo', display_name: 'Minimal Agent', description: 'Minimal description', tools: [], version: 'v1', }; mockOctoKitService.setCustomAgents([mockAgent]); const mockDetails: CustomAgentDetails = { ...mockAgent, prompt: 'Minimal prompt', }; mockOctoKitService.setAgentDetails('minimal_agent', mockDetails); await provider.provideCustomAgents({}, {} as any); await waitForPolling(); const content = await resourcesService.readCacheFile(PromptsType.agent, 'testorg', 'minimal_agent.agent.md'); assert.ok(content, 'Agent file should exist'); // Should have name and description, but no tools (empty array) assert.ok(content.includes('name: Minimal Agent')); assert.ok(content.includes('description: Minimal description')); assert.ok(!content.includes('tools:')); assert.ok(!content.includes('argument-hint:')); assert.ok(!content.includes('target:')); assert.ok(!content.includes('model:')); assert.ok(!content.includes('disable-model-invocation:')); }); test('excludes tools field when array contains only wildcard', async () => { const provider = createProvider(); const mockAgent: CustomAgentListItem = { name: 'wildcard_agent', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 1, repo_name: 'testrepo', display_name: 'Wildcard Agent', description: 'Agent with wildcard tools', tools: ['*'], version: 'v1', }; mockOctoKitService.setCustomAgents([mockAgent]); const mockDetails: CustomAgentDetails = { ...mockAgent, prompt: 'Wildcard prompt', }; mockOctoKitService.setAgentDetails('wildcard_agent', mockDetails); await provider.provideCustomAgents({}, {} as any); await waitForPolling(); const content = await resourcesService.readCacheFile(PromptsType.agent, 'testorg', 'wildcard_agent.agent.md'); assert.ok(content, 'Agent file should exist'); // Tools field should be excluded when it's just ['*'] assert.ok(!content.includes('tools:')); }); // todo: MockFileSystemService previously had a bug where deleted files would // still show up when listing directories. This was fixed and caused this test // to fail: agent files are cleared from the cache in the first poll test.skip('handles malformed frontmatter in cached files', async () => { // Prevent background fetch from interfering mockOctoKitService.setUserOrganizations([]); mockWorkspaceService.setWorkspaceFolders([]); // Pre-populate cache with mixed valid and malformed content BEFORE creating provider const validContent = `--- name: Valid Agent description: A valid agent --- Valid prompt`; // File without frontmatter - parser extracts name from filename, description is empty const noFrontmatterContent = `Just some content without any frontmatter`; prepopulateCache('testorg', new Map([ ['valid_agent.agent.md', validContent], ['no_frontmatter.agent.md', noFrontmatterContent], ])); // Re-enable testorg for cache reading mockOctoKitService.setUserOrganizations(['testorg']); const provider = createProvider(); // Wait for initial poll (which uses testorg) await waitForPolling(); const agents = await provider.provideCustomAgents({}, {} as any); // Parser is lenient - both agents are returned, one with empty description assert.equal(agents.length, 2); const validAgentName = agents[0].uri.path.split('/').pop()?.replace('.agent.md', ''); assert.equal(validAgentName, 'valid_agent'); const noFrontmatterAgentName = agents[1].uri.path.split('/').pop()?.replace('.agent.md', ''); assert.equal(noFrontmatterAgentName, 'no_frontmatter'); }); test('fetches agents from preferred organization only', async () => { // The service only fetches from the preferred organization, not all user organizations. // Preferred org is determined by workspace repository or first user organization. const provider = createProvider(); // Set up multiple organizations - testorg is the default preferred org mockOctoKitService.setUserOrganizations(['testorg', 'otherorg1', 'otherorg2']); const capturedOrgs: string[] = []; mockOctoKitService.getCustomAgents = async (owner: string, repo: string) => { capturedOrgs.push(owner); return []; }; await provider.provideCustomAgents({}, {} as any); await waitForPolling(); // Should have fetched from only the preferred organization assert.equal(capturedOrgs.length, 1); assert.ok(capturedOrgs.includes('testorg')); }); test('generates markdown with long description on single line', async () => { const provider = createProvider(); // Agent with a very long description that would normally be wrapped at 80 characters const longDescription = 'Just for fun agent that teaches computer science concepts (while pretending to plot world domination).'; const mockAgent: CustomAgentListItem = { name: 'world_domination', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 1, repo_name: 'testrepo', display_name: 'World Domination', description: longDescription, tools: [], version: 'v1', }; mockOctoKitService.setCustomAgents([mockAgent]); const mockDetails: CustomAgentDetails = { ...mockAgent, prompt: '# World Domination Agent\n\nYou are a world-class computer scientist.', }; mockOctoKitService.setAgentDetails('world_domination', mockDetails); await provider.provideCustomAgents({}, {} as any); await waitForPolling(); const content = await resourcesService.readCacheFile(PromptsType.agent, 'testorg', 'world_domination.agent.md'); const expectedContent = `--- name: World Domination description: Just for fun agent that teaches computer science concepts (while pretending to plot world domination). --- # World Domination Agent You are a world-class computer scientist. `; assert.equal(content, expectedContent); }); test('generates markdown with special characters properly escaped in description', async () => { const provider = createProvider(); // Agent with description containing YAML special characters that need proper handling const descriptionWithSpecialChars = `Agent with "double quotes", 'single quotes', colons:, and #comments in the description`; const mockAgent: CustomAgentListItem = { name: 'special_chars_agent', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 1, repo_name: 'testrepo', display_name: 'Special Chars Agent', description: descriptionWithSpecialChars, tools: [], version: 'v1', }; mockOctoKitService.setCustomAgents([mockAgent]); const mockDetails: CustomAgentDetails = { ...mockAgent, prompt: 'Test prompt with special characters', }; mockOctoKitService.setAgentDetails('special_chars_agent', mockDetails); await provider.provideCustomAgents({}, {} as any); await waitForPolling(); const content = await resourcesService.readCacheFile(PromptsType.agent, 'testorg', 'special_chars_agent.agent.md'); const expectedContent = `--- name: Special Chars Agent description: "Agent with \\"double quotes\\", 'single quotes', colons:, and #comments in the description" --- Test prompt with special characters `; assert.equal(content, expectedContent); }); test('generates markdown with multiline description containing newlines', async () => { const provider = createProvider(); // Agent with description containing actual newline characters const descriptionWithNewlines = 'First line of description.\nSecond line of description.\nThird line.'; const mockAgent: CustomAgentListItem = { name: 'multiline_agent', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 1, repo_name: 'testrepo', display_name: 'Multiline Agent', description: descriptionWithNewlines, tools: [], version: 'v1', }; mockOctoKitService.setCustomAgents([mockAgent]); const mockDetails: CustomAgentDetails = { ...mockAgent, prompt: 'Test prompt', }; mockOctoKitService.setAgentDetails('multiline_agent', mockDetails); await provider.provideCustomAgents({}, {} as any); await waitForPolling(); const content = await resourcesService.readCacheFile(PromptsType.agent, 'testorg', 'multiline_agent.agent.md'); // Newlines should be escaped using double quotes to keep description on a single line // (the custom YAML parser doesn't support multi-line strings) const expectedContent = `--- name: Multiline Agent description: "First line of description.\\nSecond line of description.\\nThird line." --- Test prompt `; assert.equal(content, expectedContent); }); test('aborts fetch if user signs out during process', async () => { const provider = createProvider(); // Setup multiple organizations to ensure we have multiple steps mockOctoKitService.setUserOrganizations(['org1', 'org2']); mockOctoKitService.getOrganizationRepositories = async (org) => ['repo']; // Mock getCustomAgents to simulate sign out after first org let callCount = 0; const originalGetCustomAgents = mockOctoKitService.getCustomAgents; mockOctoKitService.getCustomAgents = async (owner, repo, options) => { callCount++; if (callCount === 1) { // Sign out user after first call mockOctoKitService.getCurrentAuthedUser = async () => undefined as any; } return originalGetCustomAgents.call(mockOctoKitService, owner, repo, options, { createIfNone: false }); }; await provider.provideCustomAgents({}, {} as any); await waitForPolling(); // Should have aborted after first org, so second org shouldn't be processed assert.equal(callCount, 1); }); test('deduplicates enterprise agents that appear in multiple organizations', async () => { // Setup multiple organizations BEFORE creating provider mockOctoKitService.setUserOrganizations(['orgA', 'orgB']); // Clear default workspace so getPreferredOrganizationName falls back to user organizations mockWorkspaceService.setWorkspaceFolders([]); // Create an enterprise agent that will appear in both organizations const enterpriseAgent: CustomAgentListItem = { name: 'enterprise_agent', repo_owner_id: 999, repo_owner: 'enterprise_org', repo_id: 123, repo_name: 'enterprise_repo', display_name: 'Enterprise Agent', description: 'Shared enterprise agent', tools: [], version: 'v1.0', }; // Mock getCustomAgents to return the same enterprise agent for both orgs mockOctoKitService.getCustomAgents = async (owner: string, repo: string) => { // Both orgs return the same enterprise agent (same repo_owner, repo_name, name, version) return [enterpriseAgent]; }; mockOctoKitService.setAgentDetails('enterprise_agent', { ...enterpriseAgent, prompt: 'Enterprise prompt', }); const provider = createProvider(); await waitForPolling(); const agents = await provider.provideCustomAgents({}, {} as any); // Should only have one agent, not two (deduped) assert.equal(agents.length, 1); const enterpriseAgentName = agents[0].uri.path.split('/').pop()?.replace('.agent.md', ''); assert.equal(enterpriseAgentName, 'enterprise_agent'); // Verify it was only written to one org directory // Check which org has the agent file const orgAContent = await resourcesService.readCacheFile(PromptsType.agent, 'orga', 'enterprise_agent.agent.md'); const orgBContent = await resourcesService.readCacheFile(PromptsType.agent, 'orgb', 'enterprise_agent.agent.md'); const orgAHasAgent = orgAContent !== undefined; const orgBHasAgent = orgBContent !== undefined; // Agent should be in exactly one org directory (the first one processed) assert.ok(orgAHasAgent && !orgBHasAgent, 'Enterprise agent should only be cached in first org'); }); test('deduplicates agents with same repo regardless of version', async () => { // Set up mocks BEFORE creating provider mockOctoKitService.setUserOrganizations(['orgA', 'orgB']); // Create agents with same name but different versions const agentV1: CustomAgentListItem = { name: 'versioned_agent', repo_owner_id: 999, repo_owner: 'enterprise_org', repo_id: 123, repo_name: 'enterprise_repo', display_name: 'Versioned Agent', description: 'Agent version 1', tools: [], version: 'v1.0', }; const agentV2: CustomAgentListItem = { name: 'versioned_agent', repo_owner_id: 999, repo_owner: 'enterprise_org', repo_id: 123, repo_name: 'enterprise_repo', display_name: 'Versioned Agent', description: 'Agent version 2', tools: [], version: 'v2.0', }; let callCount = 0; mockOctoKitService.getCustomAgents = async (owner: string, repo: string) => { callCount++; if (callCount === 1) { // First org returns v1 and v2 return [agentV1, agentV2]; } else { // Second org also returns both versions return [agentV1, agentV2]; } }; mockOctoKitService.getCustomAgentDetails = async (owner: string, repo: string, agentName: string, version?: string) => { if (version === 'v1.0') { return { ...agentV1, prompt: 'Version 1 prompt' }; } else if (version === 'v2.0') { return { ...agentV2, prompt: 'Version 2 prompt' }; } return undefined; }; const provider = createProvider(); await waitForPolling(); const agents = await provider.provideCustomAgents({}, {} as any); // Different versions are deduplicated, only the first one is kept assert.equal(agents.length, 1); const versionedAgentName = agents[0].uri.path.split('/').pop()?.replace('.agent.md', ''); assert.equal(versionedAgentName, 'versioned_agent'); }); test('handles agents with same name but different repo owners from single org', async () => { // Set up mocks BEFORE creating provider // This tests the case where a single org returns agents from different repo owners // (e.g., an org-specific agent and an enterprise agent with the same name) mockOctoKitService.setUserOrganizations(['testorg']); // Agents with same name but different repo owners as returned by API for single org const orgAAgent: CustomAgentListItem = { name: 'shared_agent', repo_owner_id: 1, repo_owner: 'testorg', repo_id: 10, repo_name: 'org_repo', display_name: 'Org Agent', description: 'Agent from org repo', tools: [], version: 'v1.0', }; const enterpriseAgent: CustomAgentListItem = { name: 'shared_agent', repo_owner_id: 999, repo_owner: 'enterprise_org', repo_id: 100, repo_name: 'enterprise_repo', display_name: 'Enterprise Agent', description: 'Agent from enterprise', tools: [], version: 'v1.0', }; // API returns both agents for single org (enterprise agents are included via includeSources) mockOctoKitService.getCustomAgents = async (owner: string, repo: string) => { return [orgAAgent, enterpriseAgent]; }; mockOctoKitService.getCustomAgentDetails = async (owner: string, repo: string, agentName: string, version?: string) => { // The API is called with the repo_owner, not the org name if (owner === 'testorg') { return { ...orgAAgent, prompt: 'Org prompt' }; } else if (owner === 'enterprise_org') { return { ...enterpriseAgent, prompt: 'Enterprise prompt' }; } return undefined; }; const provider = createProvider(); await waitForPolling(); const agents = await provider.provideCustomAgents({}, {} as any); // Since both agents have the same name, only one file is written (last one wins) // The filename is just `${agent.name}.agent.md`, so both would write to same file assert.equal(agents.length, 1); const agentName = agents[0].uri.path.split('/').pop()?.replace('.agent.md', ''); assert.equal(agentName, 'shared_agent'); }); test('deduplicates enterprise agents even when API returns them in different order', async () => { // Set up mocks BEFORE creating provider mockOctoKitService.setUserOrganizations(['orgA', 'orgB', 'orgC']); const enterpriseAgent1: CustomAgentListItem = { name: 'enterprise_agent1', repo_owner_id: 999, repo_owner: 'enterprise_org', repo_id: 123, repo_name: 'enterprise_repo', display_name: 'Enterprise Agent 1', description: 'First enterprise agent', tools: [], version: 'v1.0', }; const enterpriseAgent2: CustomAgentListItem = { name: 'enterprise_agent2', repo_owner_id: 999, repo_owner: 'enterprise_org', repo_id: 123, repo_name: 'enterprise_repo', display_name: 'Enterprise Agent 2', description: 'Second enterprise agent', tools: [], version: 'v1.0', }; let callCount = 0; mockOctoKitService.getCustomAgents = async (owner: string, repo: string) => { callCount++; // Return agents in different orders for different orgs if (callCount === 1) { return [enterpriseAgent1, enterpriseAgent2]; } else if (callCount === 2) { return [enterpriseAgent2, enterpriseAgent1]; // Reversed order } else { return [enterpriseAgent1, enterpriseAgent2]; } }; mockOctoKitService.getCustomAgentDetails = async (owner: string, repo: string, agentName: string, version?: string) => { if (agentName === 'enterprise_agent1') { return { ...enterpriseAgent1, prompt: 'Prompt 1' }; } else if (agentName === 'enterprise_agent2') { return { ...enterpriseAgent2, prompt: 'Prompt 2' }; } return undefined; }; const provider = createProvider(); await waitForPolling(); const agents = await provider.provideCustomAgents({}, {} as any); // Should have exactly 2 agents, not 6 (2 agents x 3 orgs) assert.equal(agents.length, 2); // Verify both agent names are present const agentNames = agents.map(a => a.uri.path.split('/').pop()?.replace('.agent.md', '')).sort(); assert.deepEqual(agentNames, ['enterprise_agent1', 'enterprise_agent2']); }); test('deduplication key does not include version so different versions are deduplicated', async () => { // Set up mocks BEFORE creating provider mockOctoKitService.setUserOrganizations(['orgA']); // Same agent with two different versions const agentV1: CustomAgentListItem = { name: 'multi_version_agent', repo_owner_id: 999, repo_owner: 'enterprise_org', repo_id: 123, repo_name: 'enterprise_repo', display_name: 'Multi Version Agent', description: 'Agent with multiple versions', tools: [], version: 'v1.0', }; const agentV2: CustomAgentListItem = { ...agentV1, version: 'v2.0', }; mockOctoKitService.getCustomAgents = async () => { return [agentV1, agentV2]; }; mockOctoKitService.getCustomAgentDetails = async (owner: string, repo: string, agentName: string, version?: string) => { if (version === 'v1.0') { return { ...agentV1, prompt: 'Prompt for v1' }; } else if (version === 'v2.0') { return { ...agentV2, prompt: 'Prompt for v2' }; } return undefined; }; const provider = createProvider(); await waitForPolling(); const agents = await provider.provideCustomAgents({}, {} as any); // Different versions are deduplicated, only the first one is kept assert.equal(agents.length, 1); const multiVersionAgentName = agents[0].uri.path.split('/').pop()?.replace('.agent.md', ''); assert.equal(multiVersionAgentName, 'multi_version_agent'); }); }); suite('looksLikeNumber', () => { test('returns false for empty string', () => { assert.strictEqual(looksLikeNumber(''), false); }); test('returns true for integers', () => { assert.strictEqual(looksLikeNumber('0'), true); assert.strictEqual(looksLikeNumber('123'), true); assert.strictEqual(looksLikeNumber('-456'), true); }); test('returns true for decimals', () => { assert.strictEqual(looksLikeNumber('3.14'), true); assert.strictEqual(looksLikeNumber('-0.5'), true); assert.strictEqual(looksLikeNumber('.5'), true); }); test('returns false for non-numeric strings', () => { assert.strictEqual(looksLikeNumber('abc'), false); assert.strictEqual(looksLikeNumber('12abc'), false); assert.strictEqual(looksLikeNumber('hello'), false); }); test('returns false for special number representations', () => { // These don't match the regex /^-?\d*\.?\d+$/ assert.strictEqual(looksLikeNumber('1e10'), false); assert.strictEqual(looksLikeNumber('1.5e-3'), false); assert.strictEqual(looksLikeNumber('Infinity'), false); assert.strictEqual(looksLikeNumber('-Infinity'), false); assert.strictEqual(looksLikeNumber('NaN'), false); }); test('returns false for hex/octal representations', () => { assert.strictEqual(looksLikeNumber('0x1F'), false); assert.strictEqual(looksLikeNumber('0o17'), false); assert.strictEqual(looksLikeNumber('0b101'), false); }); test('returns false for strings with spaces', () => { assert.strictEqual(looksLikeNumber(' 123'), false); assert.strictEqual(looksLikeNumber('123 '), false); }); }); suite('yamlString', () => { test('returns plain string for simple text', () => { const result = yamlString('hello'); assert.strictEqual(result, 'hello'); }); test('returns plain string for text with spaces', () => { const result = yamlString('hello world'); assert.strictEqual(result, 'hello world'); }); suite('quoting for special characters', () => { test('quotes strings containing hash (comment)', () => { const result = yamlString('value with # hash'); assert.ok(result instanceof Scalar); assert.strictEqual(result.value, 'value with # hash'); assert.strictEqual(result.type, Scalar.QUOTE_SINGLE); }); test('quotes strings containing colon', () => { const result = yamlString('key: value'); assert.ok(result instanceof Scalar); assert.strictEqual(result.value, 'key: value'); }); test('quotes strings containing brackets', () => { const result = yamlString('array [1, 2]'); assert.ok(result instanceof Scalar); assert.strictEqual(result.value, 'array [1, 2]'); }); test('quotes strings containing braces', () => { const result = yamlString('object {a: 1}'); assert.ok(result instanceof Scalar); assert.strictEqual(result.value, 'object {a: 1}'); }); test('quotes strings containing comma', () => { const result = yamlString('a, b, c'); assert.ok(result instanceof Scalar); assert.strictEqual(result.value, 'a, b, c'); }); test('quotes strings containing newline', () => { const result = yamlString('line1\nline2'); assert.ok(result instanceof Scalar); assert.strictEqual(result.value, 'line1\nline2'); // Newlines require double quotes for escape sequence support assert.strictEqual(result.type, Scalar.QUOTE_DOUBLE); }); test('quotes strings containing carriage return', () => { const result = yamlString('line1\rline2'); assert.ok(result instanceof Scalar); assert.strictEqual(result.value, 'line1\rline2'); // Carriage returns require double quotes for escape sequence support assert.strictEqual(result.type, Scalar.QUOTE_DOUBLE); }); }); suite('quoting for values starting with quotes', () => { test('quotes strings starting with single quote', () => { const result = yamlString(`'quoted value`); assert.ok(result instanceof Scalar); assert.strictEqual(result.value, `'quoted value`); }); test('quotes strings starting with double quote', () => { const result = yamlString(`"quoted value`); assert.ok(result instanceof Scalar); assert.strictEqual(result.value, `"quoted value`); }); }); suite('quoting for whitespace', () => { test('quotes strings with leading space', () => { const result = yamlString(' leading space'); assert.ok(result instanceof Scalar); assert.strictEqual(result.value, ' leading space'); }); test('quotes strings with trailing space', () => { const result = yamlString('trailing space '); assert.ok(result instanceof Scalar); assert.strictEqual(result.value, 'trailing space '); }); }); suite('quoting for YAML keywords', () => { test('quotes "true" to preserve as string', () => { const result = yamlString('true'); assert.ok(result instanceof Scalar); assert.strictEqual(result.value, 'true'); }); test('quotes "false" to preserve as string', () => { const result = yamlString('false'); assert.ok(result instanceof Scalar); assert.strictEqual(result.value, 'false'); }); test('quotes "null" to preserve as string', () => { const result = yamlString('null'); assert.ok(result instanceof Scalar); assert.strictEqual(result.value, 'null'); }); test('quotes "~" to preserve as string', () => { const result = yamlString('~'); assert.ok(result instanceof Scalar); assert.strictEqual(result.value, '~'); }); test('does not quote "True" (case sensitive)', () => { const result = yamlString('True'); assert.strictEqual(result, 'True'); }); test('does not quote "FALSE" (case sensitive)', () => { const result = yamlString('FALSE'); assert.strictEqual(result, 'FALSE'); }); }); suite('quoting for numeric strings', () => { test('quotes integer strings', () => { const result = yamlString('123'); assert.ok(result instanceof Scalar); assert.strictEqual(result.value, '123'); }); test('quotes negative integers', () => { const result = yamlString('-456'); assert.ok(result instanceof Scalar); assert.strictEqual(result.value, '-456'); }); test('quotes decimal strings', () => { const result = yamlString('3.14'); assert.ok(result instanceof Scalar); assert.strictEqual(result.value, '3.14'); }); test('does not quote non-numeric strings that look similar', () => { const result = yamlString('v1.0'); assert.strictEqual(result, 'v1.0'); }); }); suite('quote type selection', () => { test('uses single quotes by default when quoting', () => { const result = yamlString('value with # hash'); assert.ok(result instanceof Scalar); assert.strictEqual(result.type, Scalar.QUOTE_SINGLE); }); test('does not quote string with only single quote (no special chars)', () => { // `it's a value` has no special YAML characters, so no quoting is needed const result = yamlString(`it's a value`); assert.strictEqual(result, `it's a value`); }); test('uses double quotes when value has single quote and special chars', () => { const result = yamlString(`it's a value: with colon`); assert.ok(result instanceof Scalar); assert.strictEqual(result.type, Scalar.QUOTE_DOUBLE); }); }); }); suite('yamlString round-trip with custom YAML parser', () => { /** * These tests verify that values processed by yamlString() can be * correctly parsed back by the custom YAML parser in yaml.ts */ function roundTrip(value: string): string | undefined { const yamlValue = yamlString(value); let yamlStr: string; if (yamlValue instanceof Scalar) { // Simulate how YAML library would stringify this if (yamlValue.type === Scalar.QUOTE_SINGLE) { yamlStr = `'${value}'`; } else { // Double quotes - need to escape internal double quotes yamlStr = `"${value.replace(/"/g, '\\"')}"`; } } else { yamlStr = value; } // Parse as a simple key-value YAML const yaml = `key: ${yamlStr}`; const parsed = parse(yaml); if (parsed?.type === 'object' && parsed.properties.length > 0) { const prop = parsed.properties[0]; if (prop.value.type === 'string') { return prop.value.value; } } return undefined; } test('round-trips plain string', () => { assert.strictEqual(roundTrip('hello world'), 'hello world'); }); test('round-trips string with hash', () => { assert.strictEqual(roundTrip('value # comment'), 'value # comment'); }); test('round-trips string with colon', () => { assert.strictEqual(roundTrip('key: value'), 'key: value'); }); test('round-trips boolean keyword as string', () => { assert.strictEqual(roundTrip('true'), 'true'); assert.strictEqual(roundTrip('false'), 'false'); }); test('round-trips null keyword as string', () => { assert.strictEqual(roundTrip('null'), 'null'); }); test('round-trips numeric string', () => { assert.strictEqual(roundTrip('123'), '123'); assert.strictEqual(roundTrip('3.14'), '3.14'); }); test('round-trips string with leading/trailing whitespace', () => { assert.strictEqual(roundTrip(' padded '), ' padded '); }); test('round-trips string with single quotes (no special chars)', () => { // Apostrophes without other special chars don't need quoting assert.strictEqual(roundTrip(`it's working`), `it's working`); }); test('round-trips string with single quotes and special chars', () => { // When both single quote and special char are present, double quotes are used assert.strictEqual(roundTrip(`it's a value: with colon`), `it's a value: with colon`); }); }); ================================================ FILE: src/extension/agents/vscode-node/test/githubOrgInstructionsProvider.spec.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { assert } from 'chai'; import { afterEach, beforeEach, suite, test, vi } from 'vitest'; import type { ExtensionContext } from 'vscode'; import { INSTRUCTION_FILE_EXTENSION, PromptsType } from '../../../../platform/customInstructions/common/promptTypes'; import { MockFileSystemService } from '../../../../platform/filesystem/node/test/mockFileSystemService'; import { MockAuthenticationService } from '../../../../platform/ignore/node/test/mockAuthenticationService'; import { MockGitService } from '../../../../platform/ignore/node/test/mockGitService'; import { MockWorkspaceService } from '../../../../platform/ignore/node/test/mockWorkspaceService'; import { ILogService } from '../../../../platform/log/common/logService'; import { DisposableStore } from '../../../../util/vs/base/common/lifecycle'; import { URI } from '../../../../util/vs/base/common/uri'; import { createExtensionUnitTestingServices } from '../../../test/node/services'; import { GitHubOrgChatResourcesService } from '../githubOrgChatResourcesService'; import { GitHubOrgInstructionsProvider } from '../githubOrgInstructionsProvider'; import { MockOctoKitService } from './mockOctoKitService'; suite('GitHubOrgInstructionsProvider', () => { let disposables: DisposableStore; let mockOctoKitService: MockOctoKitService; let mockFileSystem: MockFileSystemService; let mockGitService: MockGitService; let mockWorkspaceService: MockWorkspaceService; let mockExtensionContext: Partial; let mockAuthService: MockAuthenticationService; let accessor: any; let provider: GitHubOrgInstructionsProvider; let resourcesService: GitHubOrgChatResourcesService; const storagePath = '/tmp/test-storage'; const storageUri = URI.file(storagePath); beforeEach(() => { vi.useFakeTimers(); disposables = new DisposableStore(); // Create mocks for real GitHubOrgChatResourcesService mockOctoKitService = new MockOctoKitService(); mockFileSystem = new MockFileSystemService(); mockGitService = new MockGitService(); mockWorkspaceService = new MockWorkspaceService(); mockExtensionContext = { globalStorageUri: storageUri, }; mockAuthService = new MockAuthenticationService(); // Default: user is in 'testorg' and workspace belongs to 'testorg' mockOctoKitService.setUserOrganizations(['testorg']); mockWorkspaceService.setWorkspaceFolders([URI.file('/workspace')]); mockGitService.setRepositoryFetchUrls({ rootUri: URI.file('/workspace'), remoteFetchUrls: ['https://github.com/testorg/repo.git'] }); // Set up testing services const testingServiceCollection = createExtensionUnitTestingServices(disposables); accessor = disposables.add(testingServiceCollection.createTestingAccessor()); }); afterEach(() => { vi.useRealTimers(); disposables.dispose(); mockOctoKitService.reset(); }); function createProvider(): GitHubOrgInstructionsProvider { // Create the real GitHubOrgChatResourcesService with mocked dependencies resourcesService = new GitHubOrgChatResourcesService( mockAuthService as any, mockExtensionContext as any, mockFileSystem, mockGitService, accessor.get(ILogService), mockOctoKitService, mockWorkspaceService, ); disposables.add(resourcesService); // Create provider with real resources service provider = new GitHubOrgInstructionsProvider( accessor.get(ILogService), mockOctoKitService, resourcesService, ); disposables.add(provider); return provider; } /** * Advance timers and wait for polling callback to complete. * Uses a small time advance to trigger the initial poll without infinite loops. */ async function waitForPolling(): Promise { await vi.advanceTimersByTimeAsync(10); } /** * Helper to pre-populate cache files in mock filesystem. */ function prepopulateCache(orgName: string, files: Map): void { const cacheDir = URI.file(`${storagePath}/github/${orgName}/instructions`); const dirEntries: [string, import('../../../../platform/filesystem/common/fileTypes').FileType][] = []; for (const [filename, content] of files) { mockFileSystem.mockFile(URI.joinPath(cacheDir, filename), content); dirEntries.push([filename, 1 /* FileType.File */]); } mockFileSystem.mockDirectory(cacheDir, dirEntries); } test('returns empty array when no organization available', async () => { mockOctoKitService.setUserOrganizations([]); mockWorkspaceService.setWorkspaceFolders([]); const provider = createProvider(); const instructions = await provider.provideInstructions({}, {} as any); assert.deepEqual(instructions, []); }); test('returns cached instructions when available', async () => { const orgId = 'testorg'; // Pre-populate cache with instructions const instructionContent = '# Custom Instructions\nThese are custom instructions for the organization.'; prepopulateCache(orgId, new Map([ [`default${INSTRUCTION_FILE_EXTENSION}`, instructionContent] ])); const provider = createProvider(); const instructions = await provider.provideInstructions({}, {} as any); assert.equal(instructions.length, 1); assert.ok(instructions[0].uri.path.endsWith(`default${INSTRUCTION_FILE_EXTENSION}`)); }); test('returns empty array when cache is empty', async () => { // No cache populated const provider = createProvider(); const instructions = await provider.provideInstructions({}, {} as any); assert.deepEqual(instructions, []); }); test.skip('pollInstructions writes instructions to cache when found', async () => { const orgId = 'testorg'; const instructionContent = '# Organization Instructions\nBe helpful and concise.'; mockOctoKitService.setOrgInstructions(orgId, instructionContent); createProvider(); await waitForPolling(); // Verify the instructions were written to cache const cachedContent = await resourcesService.readCacheFile( PromptsType.instructions, orgId, `default${INSTRUCTION_FILE_EXTENSION}` ); // The implementation adds applyTo front matter to the cached content const expectedContent = `---\napplyTo: '**'\n---\n${instructionContent}`; assert.equal(cachedContent, expectedContent); }); test.skip('pollInstructions does nothing when no instructions found', async () => { mockOctoKitService.setOrgInstructions('testorg', undefined); createProvider(); await waitForPolling(); // Verify no instructions were written const cachedContent = await resourcesService.readCacheFile( PromptsType.instructions, 'testorg', `default${INSTRUCTION_FILE_EXTENSION}` ); assert.isUndefined(cachedContent); }); test.skip('fires change event when instructions content changes', async () => { const instructionContent = '# New Instructions\nUpdated content.'; mockOctoKitService.setOrgInstructions('testorg', instructionContent); const provider = createProvider(); let eventFired = false; provider.onDidChangeInstructions(() => { eventFired = true; }); await waitForPolling(); assert.isTrue(eventFired, 'Change event should fire when instructions are updated'); }); test.skip('fires change event on every successful poll with instructions', async () => { // Note: The current implementation does not pass checkForChanges option to writeCacheFile, // so change events fire on every poll even when content is unchanged const instructionContent = '# Stable Instructions\nThis content will not change.'; mockOctoKitService.setOrgInstructions('testorg', instructionContent); // Pre-populate cache with the same content prepopulateCache('testorg', new Map([ [`default${INSTRUCTION_FILE_EXTENSION}`, instructionContent] ])); const provider = createProvider(); let changeEventCount = 0; provider.onDidChangeInstructions(() => { changeEventCount++; }); await waitForPolling(); assert.equal(changeEventCount, 1, 'Change event fires on every successful poll'); }); test.skip('pollInstructions handles API errors gracefully without throwing', async () => { // Make the API throw an error mockOctoKitService.getOrgCustomInstructions = async () => { throw new Error('API Error'); }; createProvider(); // pollInstructions has internal error handling - errors are logged but not thrown // This is intentional to prevent polling failures from crashing the extension let errorThrown = false; try { await waitForPolling(); } catch (e: any) { errorThrown = true; } assert.isFalse(errorThrown, 'API errors should be handled internally and not propagate'); }); test('returns instructions from correct organization', async () => { // Pre-populate different orgs with different instructions prepopulateCache('org1', new Map([ [`default${INSTRUCTION_FILE_EXTENSION}`, 'Org1 instructions'] ])); prepopulateCache('org2', new Map([ [`default${INSTRUCTION_FILE_EXTENSION}`, 'Org2 instructions'] ])); // Set preferred org to org2 by configuring workspace git remote mockOctoKitService.setUserOrganizations(['org1', 'org2']); mockGitService.setRepositoryFetchUrls({ rootUri: URI.file('/workspace'), remoteFetchUrls: ['https://github.com/org2/repo.git'] }); const provider = createProvider(); const instructions = await provider.provideInstructions({}, {} as any); assert.equal(instructions.length, 1); // The URI should contain 'org2', not 'org1' assert.ok(instructions[0].uri.path.includes('org2')); }); test('handles cache read errors gracefully', async () => { const provider = createProvider(); // Override readDirectory to throw an error const originalReadDirectory = mockFileSystem.readDirectory.bind(mockFileSystem); mockFileSystem.readDirectory = async () => { throw new Error('Cache read error'); }; // Should not throw, should return empty array const instructions = await provider.provideInstructions({}, {} as any); assert.deepEqual(instructions, []); // Restore original method mockFileSystem.readDirectory = originalReadDirectory; }); test('respects cancellation token in provideInstructions', async () => { prepopulateCache('testorg', new Map([ [`default${INSTRUCTION_FILE_EXTENSION}`, 'Some instructions'] ])); const provider = createProvider(); // Create a cancelled token const cancelledToken = { isCancellationRequested: true, onCancellationRequested: () => ({ dispose: () => { } }) }; const instructions = await provider.provideInstructions({}, cancelledToken as any); // Should return empty array when cancelled assert.deepEqual(instructions, []); }); test('uses correct file extension for instruction files', async () => { const instructionContent = '# Test Instructions'; mockOctoKitService.setOrgInstructions('testorg', instructionContent); const provider = createProvider(); await waitForPolling(); // Verify the file was written with the correct extension const cachedContent = await resourcesService.readCacheFile( PromptsType.instructions, 'testorg', `default${INSTRUCTION_FILE_EXTENSION}` ); // The implementation adds applyTo front matter to the cached content const expectedContent = `---\napplyTo: '**'\n---\n${instructionContent}`; assert.equal(cachedContent, expectedContent); // Prepopulate so we can list it prepopulateCache('testorg', new Map([ [`default${INSTRUCTION_FILE_EXTENSION}`, instructionContent] ])); const instructions = await provider.provideInstructions({}, {} as any); assert.equal(instructions.length, 1); assert.ok(instructions[0].uri.path.endsWith(INSTRUCTION_FILE_EXTENSION)); }); test('disposes polling subscription when provider is disposed', () => { const provider = createProvider(); // Should not throw when disposed provider.dispose(); // Provider should be properly cleaned up assert.ok(true, 'Provider disposed without errors'); }); test('multiple instruction files are returned when present', async () => { // Pre-populate cache with multiple instruction files prepopulateCache('testorg', new Map([ [`default${INSTRUCTION_FILE_EXTENSION}`, 'Default instructions'], [`custom${INSTRUCTION_FILE_EXTENSION}`, 'Custom instructions'], [`team${INSTRUCTION_FILE_EXTENSION}`, 'Team instructions'], ])); const provider = createProvider(); const instructions = await provider.provideInstructions({}, {} as any); assert.equal(instructions.length, 3); }); }); ================================================ FILE: src/extension/agents/vscode-node/test/mockOctoKitService.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { CCAEnabledResult, CustomAgentDetails, CustomAgentListItem, CustomAgentListOptions, GitHubOutageStatus, IOctoKitService, PermissiveAuthRequiredError } from '../../../../platform/github/common/githubService'; /** * Mock implementation of IOctoKitService for testing */ export class MockOctoKitService implements IOctoKitService { _serviceBrand: undefined; private customAgents: CustomAgentListItem[] = []; private agentDetails: Map = new Map(); private orgInstructions: Map = new Map(); private userOrganizations: string[] = ['testorg']; getCurrentAuthedUser = async () => ({ login: 'testuser', name: 'Test User', avatar_url: '' }); getCopilotPullRequestsForUser = async () => []; getGitHubOutageStatus = async (): Promise => GitHubOutageStatus.None; getCopilotSessionsForPR = async () => []; getSessionLogs = async () => ''; getSessionInfo = async () => undefined; postCopilotAgentJob = async () => undefined; getJobByJobId = async () => undefined; getJobBySessionId = async () => undefined; addPullRequestComment = async () => null; getAllOpenSessions = async () => []; getAllSessions = async () => []; getPullRequestFromGlobalId = async () => null; getPullRequestFiles = async () => []; closePullRequest = async () => false; findPullRequestByHeadBranch = async () => undefined; getOpenPullRequestsForUser = async () => []; getFileContent = async () => ''; getUserRepositories = async () => []; getRecentlyCommittedRepositories = async () => []; getCopilotAgentModels = async () => []; getAssignableActors = async () => []; isCCAEnabled = async (): Promise => ({ enabled: true }); getUserOrganizations = async (_authOptions?: { createIfNone?: boolean }, _pageSize?: number) => this.userOrganizations; isUserMemberOfOrg = async (org: string, _authOptions?: { createIfNone?: boolean }) => this.userOrganizations.includes(org); getOrganizationRepositories = async (org: string, _authOptions?: { createIfNone?: boolean }, _pageSize?: number) => [org === 'testorg' ? 'testrepo' : 'repo']; async getOrgCustomInstructions(orgLogin: string, _authOptions?: { createIfNone?: boolean }): Promise { return this.orgInstructions.get(orgLogin); } async getCustomAgents(_owner: string, _repo: string, _options: CustomAgentListOptions, _authOptions: { createIfNone?: boolean }): Promise { if (!(await this.getCurrentAuthedUser())) { throw new PermissiveAuthRequiredError(); } return this.customAgents; } async getCustomAgentDetails(_owner: string, _repo: string, agentName: string, _version: string, _authOptions: { createIfNone?: boolean }): Promise { return this.agentDetails.get(agentName); } // Helper methods for test setup setOrgInstructions(orgLogin: string, instructions: string | undefined) { if (instructions === undefined) { this.orgInstructions.delete(orgLogin); } else { this.orgInstructions.set(orgLogin, instructions); } } clearInstructions() { this.orgInstructions.clear(); } setCustomAgents(agents: CustomAgentListItem[]) { this.customAgents = agents; } setAgentDetails(name: string, details: CustomAgentDetails) { this.agentDetails.set(name, details); } setUserOrganizations(orgs: string[]) { this.userOrganizations = orgs; } clearAgents() { this.customAgents = []; this.agentDetails.clear(); } /** * Resets all mock state */ reset() { this.clearInstructions(); this.clearAgents(); this.userOrganizations = ['testorg']; } } ================================================ FILE: src/extension/agents/vscode-node/test/planAgentProvider.spec.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { assert } from 'chai'; import * as os from 'os'; import * as path from 'path'; import { afterEach, beforeEach, suite, test } from 'vitest'; import * as vscode from 'vscode'; import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService'; import { InMemoryConfigurationService } from '../../../../platform/configuration/test/common/inMemoryConfigurationService'; import { IVSCodeExtensionContext } from '../../../../platform/extContext/common/extensionContext'; import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService'; import { MockExtensionContext } from '../../../../platform/test/node/extensionContext'; import { ITestingServicesAccessor } from '../../../../platform/test/node/services'; import { DisposableStore } from '../../../../util/vs/base/common/lifecycle'; import { SyncDescriptor } from '../../../../util/vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation'; import { createExtensionUnitTestingServices } from '../../../test/node/services'; import { buildAgentMarkdown } from '../agentTypes'; import { PlanAgentProvider } from '../planAgentProvider'; suite('PlanAgentProvider', () => { let disposables: DisposableStore; let mockConfigurationService: InMemoryConfigurationService; let fileSystemService: IFileSystemService; let accessor: ITestingServicesAccessor; let instantiationService: IInstantiationService; beforeEach(() => { disposables = new DisposableStore(); // Set up testing services with a mock extension context that has globalStorageUri const testingServiceCollection = createExtensionUnitTestingServices(disposables); const globalStoragePath = path.join(os.tmpdir(), 'plan-agent-test-' + Date.now()); testingServiceCollection.define(IVSCodeExtensionContext, new SyncDescriptor(MockExtensionContext, [globalStoragePath])); accessor = testingServiceCollection.createTestingAccessor(); disposables.add(accessor); instantiationService = accessor.get(IInstantiationService); mockConfigurationService = accessor.get(IConfigurationService) as InMemoryConfigurationService; fileSystemService = accessor.get(IFileSystemService); }); afterEach(() => { disposables.dispose(); }); function createProvider() { const provider = instantiationService.createInstance(PlanAgentProvider); disposables.add(provider); return provider; } async function getAgentContent(agent: vscode.ChatResource): Promise { const content = await fileSystemService.readFile(agent.uri); return new TextDecoder().decode(content); } test('provideCustomAgents() returns a Plan agent with correct structure', async () => { const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); assert.ok(agents[0].uri, 'Agent should have a URI'); assert.ok(agents[0].uri.path.endsWith('.agent.md'), 'Agent URI should end with .agent.md'); }); test('returns agent content with base frontmatter when no settings configured', async () => { const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); // Should contain base tools assert.ok(content.includes('github/issue_read')); assert.ok(content.includes('agent')); assert.ok(content.includes('search')); assert.ok(content.includes('read')); assert.ok(content.includes('memory')); // Should not have model override (not in base content) assert.ok(content.includes('name: Plan')); assert.ok(content.includes('description: Researches and outlines multi-step plans')); }); test('merges additionalTools setting with base tools', async () => { await mockConfigurationService.setConfig(ConfigKey.PlanAgentAdditionalTools, ['customTool1', 'customTool2']); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); // Should contain base tools assert.ok(content.includes('github/issue_read')); assert.ok(content.includes('agent')); // Should contain additional tools assert.ok(content.includes('customTool1')); assert.ok(content.includes('customTool2')); }); test('deduplicates tools when additionalTools overlaps with base tools', async () => { // Add a tool that already exists in base await mockConfigurationService.setConfig(ConfigKey.PlanAgentAdditionalTools, ['agent', 'newTool']); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); // Count occurrences of 'agent' in tools list (flow-style array) // Should appear only once due to deduplication const toolsMatch = content.match(/tools: \[([^\]]+)\]/); assert.ok(toolsMatch, 'Tools list not found in agent content'); const toolsSection = toolsMatch[1]; const agentCount = (toolsSection.match(/'agent'/g) || []).length; assert.equal(agentCount, 1, 'agent tool should appear only once after deduplication'); // Should contain new tool assert.ok(content.includes('newTool')); }); test('applies model override from settings', async () => { await mockConfigurationService.setConfig(ConfigKey.Deprecated.PlanAgentModel, 'Claude Haiku 4.5 (copilot)'); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); // Should contain model override assert.ok(content.includes('model: Claude Haiku 4.5 (copilot)')); }); test('applies core default model when configured', async () => { await mockConfigurationService.setNonExtensionConfig('chat.planAgent.defaultModel', 'Claude Haiku 4.5 (copilot)'); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); // Should contain model override from core setting assert.ok(content.includes('model: Claude Haiku 4.5 (copilot)')); }); test('prefers core default model over extension setting', async () => { await mockConfigurationService.setNonExtensionConfig('chat.planAgent.defaultModel', 'core-model'); await mockConfigurationService.setConfig(ConfigKey.Deprecated.PlanAgentModel, 'extension-model'); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); // Should contain core model override assert.ok(content.includes('model: core-model')); assert.ok(!content.includes('model: extension-model')); }); test('applies both additionalTools and model settings together', async () => { await mockConfigurationService.setConfig(ConfigKey.PlanAgentAdditionalTools, ['extraTool']); await mockConfigurationService.setConfig(ConfigKey.Deprecated.PlanAgentModel, 'claude-3-sonnet'); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); // Should contain additional tool assert.ok(content.includes('extraTool')); // Should contain model override assert.ok(content.includes('model: claude-3-sonnet')); }); test('fires onDidChangeCustomAgents when additionalTools setting changes', async () => { const provider = createProvider(); let eventFired = false; provider.onDidChangeCustomAgents(() => { eventFired = true; }); await mockConfigurationService.setConfig(ConfigKey.PlanAgentAdditionalTools, ['newTool']); assert.equal(eventFired, true); }); test('fires onDidChangeCustomAgents when model setting changes', async () => { const provider = createProvider(); let eventFired = false; provider.onDidChangeCustomAgents(() => { eventFired = true; }); await mockConfigurationService.setConfig(ConfigKey.Deprecated.PlanAgentModel, 'new-model'); assert.equal(eventFired, true); }); test('fires onDidChangeCustomAgents when core default model changes', async () => { const provider = createProvider(); let eventFired = false; provider.onDidChangeCustomAgents(() => { eventFired = true; }); await mockConfigurationService.setNonExtensionConfig('chat.planAgent.defaultModel', 'core-model'); assert.equal(eventFired, true); }); test('does not fire onDidChangeCustomAgents for unrelated setting changes', async () => { const provider = createProvider(); let eventFired = false; provider.onDidChangeCustomAgents(() => { eventFired = true; }); // Set an unrelated config (using a different config key) await mockConfigurationService.setConfig(ConfigKey.Advanced.FeedbackOnChange, true); assert.equal(eventFired, false); }); test('always includes askQuestions tool in generated content', async () => { const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); assert.ok(content.includes('vscode/askQuestions')); }); test('has correct label property', () => { const provider = createProvider(); assert.ok(provider.label.includes('Plan')); }); test('preserves body content after frontmatter when applying settings', async () => { await mockConfigurationService.setConfig(ConfigKey.Deprecated.PlanAgentModel, 'test-model'); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); const content = await getAgentContent(agents[0]); // Should preserve body content assert.ok(content.includes('You are a PLANNING AGENT, pairing with the user')); assert.ok(content.includes('Your SOLE responsibility is planning. NEVER start implementation.')); }); test('handles empty additionalTools array gracefully', async () => { await mockConfigurationService.setConfig(ConfigKey.PlanAgentAdditionalTools, []); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); // Should have base tools only assert.ok(content.includes('github/issue_read')); assert.ok(content.includes('agent')); }); test('handles empty model string gracefully', async () => { await mockConfigurationService.setConfig(ConfigKey.Deprecated.PlanAgentModel, ''); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); // Should not have model field added assert.ok(!content.includes('model:')); }); test('falls back to extension setting when core default model is empty string', async () => { await mockConfigurationService.setNonExtensionConfig('chat.planAgent.defaultModel', ''); await mockConfigurationService.setConfig(ConfigKey.Deprecated.PlanAgentModel, 'fallback-model'); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); // Empty core setting should fall through to extension setting assert.ok(content.includes('model: fallback-model')); }); test('includes handoffs in generated content', async () => { const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); const content = await getAgentContent(agents[0]); // Should contain handoffs assert.ok(content.includes('handoffs:')); assert.ok(content.includes('label: Start Implementation')); assert.ok(content.includes('label: Open in Editor')); assert.ok(content.includes('agent: agent')); assert.ok(content.includes('send: true')); }); test('applies ImplementAgentModel to Start Implementation handoff', async () => { await mockConfigurationService.setConfig(ConfigKey.ImplementAgentModel, 'Claude Haiku 4.5 (copilot)'); const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); assert.equal(agents.length, 1); const content = await getAgentContent(agents[0]); // Should contain Start Implementation handoff with model override assert.ok(content.includes('label: Start Implementation')); assert.ok(content.includes('model: Claude Haiku 4.5 (copilot)')); }); test('does not include model in handoff when ImplementAgentModel is not set', async () => { const provider = createProvider(); const agents = await provider.provideCustomAgents({}, {} as any); const content = await getAgentContent(agents[0]); // Find the Start Implementation handoff section const handoffsStart = content.indexOf('handoffs:'); const handoffsSection = content.slice(handoffsStart, content.indexOf('---', handoffsStart)); // Should not contain model field in handoffs when not configured assert.ok(!handoffsSection.includes('model:'), 'Should not have model field in handoffs when ImplementAgentModel is not set'); }); test('fires onDidChangeCustomAgents when ImplementAgentModel setting changes', async () => { const provider = createProvider(); let eventFired = false; provider.onDidChangeCustomAgents(() => { eventFired = true; }); await mockConfigurationService.setConfig(ConfigKey.ImplementAgentModel, 'new-model'); assert.equal(eventFired, true); }); }); suite('buildAgentMarkdown', () => { test('generates expected full content for Plan agent (snapshot test)', () => { // This test outputs the full generated content for easy visual review of format changes const config = { name: 'Plan', description: 'Researches and outlines multi-step plans', argumentHint: 'Outline the goal or problem to research', tools: ['github/issue_read', 'agent', 'search', 'memory'], model: 'Claude Haiku 4.5 (copilot)', handoffs: [ { label: 'Start Implementation', agent: 'agent', prompt: 'Start implementation', send: true } ], body: 'You are a PLANNING AGENT.' }; const result = buildAgentMarkdown(config); assert.deepStrictEqual(result, `--- name: Plan description: Researches and outlines multi-step plans argument-hint: Outline the goal or problem to research model: Claude Haiku 4.5 (copilot) tools: ['github/issue_read', 'agent', 'search', 'memory'] handoffs: - label: Start Implementation agent: agent prompt: 'Start implementation' send: true --- You are a PLANNING AGENT.`); }); test('generates valid YAML frontmatter with basic config', () => { const config = { name: 'TestAgent', description: 'Test description', argumentHint: 'Test hint', tools: ['tool1', 'tool2'], handoffs: [], body: 'Test body content' }; const result = buildAgentMarkdown(config); assert.ok(result.startsWith('---\n')); assert.ok(result.includes('name: TestAgent')); assert.ok(result.includes('description: Test description')); assert.ok(result.includes('argument-hint: Test hint')); assert.ok(result.includes('tools: [\'tool1\', \'tool2\']')); assert.ok(result.includes('---\nTest body content')); }); test('includes model when provided', () => { const config = { name: 'TestAgent', description: 'Test', argumentHint: 'Test', tools: [], model: 'Claude Haiku 4.5 (copilot)', handoffs: [], body: 'Body' }; const result = buildAgentMarkdown(config); assert.ok(result.includes('model: Claude Haiku 4.5 (copilot)')); }); test('omits model when not provided', () => { const config = { name: 'TestAgent', description: 'Test', argumentHint: 'Test', tools: [], handoffs: [], body: 'Body' }; const result = buildAgentMarkdown(config); assert.ok(!result.includes('model:')); }); test('generates handoffs in block style', () => { const config = { name: 'TestAgent', description: 'Test', argumentHint: 'Test', tools: [], handoffs: [ { label: 'Continue', agent: 'agent', prompt: 'Do the thing', send: true }, { label: 'Save', agent: 'editor', prompt: 'Save it', showContinueOn: false } ], body: 'Body' }; const result = buildAgentMarkdown(config); assert.ok(result.includes('handoffs:')); assert.ok(result.includes(' - label: Continue')); assert.ok(result.includes(' agent: agent')); assert.ok(result.includes(' prompt: \'Do the thing\'')); assert.ok(result.includes(' send: true')); assert.ok(result.includes(' - label: Save')); assert.ok(result.includes(' prompt: \'Save it\'')); assert.ok(result.includes(' showContinueOn: false')); }); test('handles empty tools array', () => { const config = { name: 'TestAgent', description: 'Test', argumentHint: 'Test', tools: [], handoffs: [], body: 'Body' }; const result = buildAgentMarkdown(config); // Should not have tools line when empty assert.ok(!result.includes('tools:')); }); test('quotes tool names in flow-style array', () => { const config = { name: 'TestAgent', description: 'Test', argumentHint: 'Test', tools: ['github/issue_read', 'mcp_server/custom_tool'], handoffs: [], body: 'Body' }; const result = buildAgentMarkdown(config); assert.ok(result.includes('tools: [\'github/issue_read\', \'mcp_server/custom_tool\']')); }); test('escapes single quotes in tool names', () => { const config = { name: 'TestAgent', description: 'Test', argumentHint: 'Test', tools: ['tool\'s_name', 'another'], handoffs: [], body: 'Body' }; const result = buildAgentMarkdown(config); // Single quotes should be doubled for YAML escaping assert.ok(result.includes('\'tool\'\'s_name\''), 'Single quote should be escaped by doubling'); }); test('escapes single quotes in handoff prompts', () => { const config = { name: 'TestAgent', description: 'Test', argumentHint: 'Test', tools: [], handoffs: [ { label: 'Test', agent: 'agent', prompt: 'It\'s a test prompt with \'quotes\'' } ], body: 'Body' }; const result = buildAgentMarkdown(config); // Single quotes in prompt should be doubled for YAML escaping assert.ok(result.includes('prompt: \'It\'\'s a test prompt with \'\'quotes\'\'\''), 'Single quotes should be escaped by doubling'); }); }); ================================================ FILE: src/extension/agents/vscode-node/troubleshootSkillProvider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; import { ILogService } from '../../../platform/log/common/logService'; import { BaseSkillProvider } from './baseSkillProvider'; const RUNTIME_CONTEXT_PLACEHOLDER = '{{DEBUG_LOG_RUNTIME_CONTEXT}}'; export class TroubleshootSkillProvider extends BaseSkillProvider { constructor( @ILogService logService: ILogService, @IVSCodeExtensionContext extensionContext: IVSCodeExtensionContext, ) { super(logService, extensionContext, 'troubleshoot'); } private getRuntimeContext(): string { const lines: string[] = []; lines.push('## Runtime Log Context'); lines.push(''); // Provide the debug-logs directory path so the agent can find log files. // The {{CURRENT_SESSION_LOG}} placeholder may be resolved earlier during prompt // rendering (for example by PromptFile.getBodyContent) or later by the read_file // tool, which has access to the correct session context. const storageUri = this.extensionContext.storageUri; if (storageUri) { lines.push('- Current session log directory: `{{CURRENT_SESSION_LOG}}`'); } else { lines.push('- Debug-logs directory: unavailable in this environment. Abort now and tell the user that troubleshooting is only available if a workspace is open.'); } return lines.join('\n'); } protected override processTemplate(templateContent: string): string { return templateContent.replace(RUNTIME_CONTEXT_PLACEHOLDER, this.getRuntimeContext()); } } ================================================ FILE: src/extension/api/vscode/api.d.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { TextEditor } from 'vscode'; /** * The API provided by the Copilot extension. */ export interface CopilotExtensionApi { /** * * @param editor - The optional text editor to select the scope in. If not provided, the active text editor will be used. * @param options - Additional options for selecting the scope. * @param options.reason - The reason for selecting the scope. Will be used in the placeholder hint. * @returns A promise that resolves to the selected scope as a `Selection` object, or `undefined` if no scope was selected. */ selectScope: (editor?: TextEditor, options?: { reason?: string }) => Promise; } ================================================ FILE: src/extension/api/vscode/extensionApi.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { TextEditor, window } from 'vscode'; import { Copilot } from '../../../platform/inlineCompletions/common/api'; import { ILanguageContextProviderService } from '../../../platform/languageContextProvider/common/languageContextProviderService'; import { IScopeSelector } from '../../../platform/scopeSelection/common/scopeSelection'; import { CopilotExtensionApi as ICopilotExtensionApi } from './api'; import { VSCodeContextProviderApiV1 } from './vscodeContextProviderApi'; export class CopilotExtensionApi implements ICopilotExtensionApi { public static readonly version = 1; constructor( @IScopeSelector private readonly _scopeSelector: IScopeSelector, @ILanguageContextProviderService private readonly _languageContextProviderService: ILanguageContextProviderService ) { } async selectScope(editor?: TextEditor, options?: { reason?: string }) { editor ??= window.activeTextEditor; if (!editor) { return; } return this._scopeSelector.selectEnclosingScope(editor, options); } getContextProviderAPI(_version: 'v1'): Copilot.ContextProviderApiV1 { return new VSCodeContextProviderApiV1(this._languageContextProviderService); } } ================================================ FILE: src/extension/api/vscode/vscodeContextProviderApi.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vscode'; import { Copilot } from '../../../platform/inlineCompletions/common/api'; import { ILanguageContextProviderService, ProviderTarget } from '../../../platform/languageContextProvider/common/languageContextProviderService'; export class VSCodeContextProviderApiV1 implements Copilot.ContextProviderApiV1 { constructor( @ILanguageContextProviderService private contextProviderService: ILanguageContextProviderService, ) { } registerContextProvider(provider: Copilot.ContextProvider): Disposable { return this.contextProviderService.registerContextProvider(provider, [ProviderTarget.Completions]); } } ================================================ FILE: src/extension/authentication/vscode-node/authentication.contribution.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { commands, window } from 'vscode'; import { IAuthenticationService } from '../../../platform/authentication/common/authentication'; import { IAuthenticationChatUpgradeService } from '../../../platform/authentication/common/authenticationUpgrade'; import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; import { ILogService } from '../../../platform/log/common/logService'; import { Event } from '../../../util/vs/base/common/event'; import { Disposable } from '../../../util/vs/base/common/lifecycle'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; /** * The main entry point for the authentication contribution. */ export class AuthenticationContrib extends Disposable { constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) { super(); this.askToUpgradeAuthPermissions(); } private async askToUpgradeAuthPermissions() { const authUpgradeAsk = this._register(this.instantiationService.createInstance(AuthUpgradeAsk)); await authUpgradeAsk.run(); } } /** * This contribution ensures we have a token that is good enough for making API calls for current workspace. */ class AuthUpgradeAsk extends Disposable { private static readonly AUTH_UPGRADE_ASK_KEY = 'copilot.shownPermissiveTokenModal'; constructor( @IAuthenticationService private readonly _authenticationService: IAuthenticationService, @ILogService private readonly _logService: ILogService, @IVSCodeExtensionContext private readonly _extensionContext: IVSCodeExtensionContext, @IAuthenticationChatUpgradeService private readonly _authenticationChatUpgradeService: IAuthenticationChatUpgradeService, ) { super(); this._register(commands.registerCommand('github.copilot.chat.triggerPermissiveSignIn', async () => { await this._authenticationChatUpgradeService.showPermissiveSessionModal(true); })); } async run() { await this.waitForChatEnabled(); this.registerListeners(); await this.showPrompt(); } private async waitForChatEnabled() { try { await this._authenticationService.getCopilotToken(); } catch (error) { // likely due to the user canceling the auth flow this._logService.error(error, 'Failed to get copilot token'); } await Event.toPromise( Event.filter( this._authenticationService.onDidAuthenticationChange, () => this._authenticationService.copilotToken !== undefined ) ); } private registerListeners() { this._register(this._authenticationService.onDidAuthenticationChange(async () => { if (this._authenticationService.permissiveGitHubSession) { return; } if (!this._authenticationService.anyGitHubSession) { // We signed out, so we should show the prompt again this._extensionContext.globalState.update(AuthUpgradeAsk.AUTH_UPGRADE_ASK_KEY, false); return; } if (window.state.focused) { await this.showPrompt(); } else { // Wait for the window to get focus before trying to show the prompt const disposable = window.onDidChangeWindowState(async (e) => { if (e.focused) { disposable.dispose(); await this.showPrompt(); } }); } })); } private async showPrompt() { if ( // Already asked in a previous session this._extensionContext.globalState.get(AuthUpgradeAsk.AUTH_UPGRADE_ASK_KEY, false) // Some other criteria for not showing the prompt || !(await this._authenticationChatUpgradeService.shouldRequestPermissiveSessionUpgrade()) ) { return; } if (await this._authenticationChatUpgradeService.showPermissiveSessionModal()) { this._logService.debug('Got permissive GitHub token'); } else { this._logService.debug('Did not get permissive GitHub token'); } this._extensionContext.globalState.update(AuthUpgradeAsk.AUTH_UPGRADE_ASK_KEY, true); } } ================================================ FILE: src/extension/byok/common/anthropicMessageConverter.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ContentBlockParam, ImageBlockParam, MessageParam, RedactedThinkingBlockParam, TextBlockParam, ThinkingBlockParam } from '@anthropic-ai/sdk/resources'; import { Raw } from '@vscode/prompt-tsx'; import type { LanguageModelChatMessage } from 'vscode'; import { CustomDataPartMimeTypes } from '../../../platform/endpoint/common/endpointTypes'; import { isDefined } from '../../../util/vs/base/common/types'; import { LanguageModelChatMessageRole, LanguageModelDataPart, LanguageModelTextPart, LanguageModelThinkingPart, LanguageModelToolCallPart, LanguageModelToolResultPart, LanguageModelToolResultPart2 } from '../../../vscodeTypes'; function apiContentToAnthropicContent(content: (LanguageModelTextPart | LanguageModelToolResultPart | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelThinkingPart)[]): ContentBlockParam[] { const convertedContent: ContentBlockParam[] = []; for (const part of content) { if (part instanceof LanguageModelThinkingPart) { // Check if this is a redacted thinking block if (part.metadata?.redactedData) { convertedContent.push({ type: 'redacted_thinking', data: part.metadata.redactedData, }); } else if (part.metadata?._completeThinking) { // Only push thinking block when we have the complete thinking marker convertedContent.push({ type: 'thinking', thinking: part.metadata._completeThinking, signature: part.metadata.signature || '', }); } // Skip incremental thinking parts - we only care about the complete one } else if (part instanceof LanguageModelToolCallPart) { convertedContent.push({ type: 'tool_use', id: part.callId, input: part.input, name: part.name, }); } else if (part instanceof LanguageModelDataPart && part.mimeType === CustomDataPartMimeTypes.CacheControl && part.data.toString() === 'ephemeral') { const previousBlock = convertedContent.at(-1); if (previousBlock && contentBlockSupportsCacheControl(previousBlock)) { previousBlock.cache_control = { type: 'ephemeral' }; } else { // Empty string is invalid convertedContent.push({ type: 'text', text: ' ', cache_control: { type: 'ephemeral' } }); } } else if (part instanceof LanguageModelDataPart) { if (part.mimeType !== CustomDataPartMimeTypes.StatefulMarker) { convertedContent.push({ type: 'image', source: { type: 'base64', data: Buffer.from(part.data).toString('base64'), media_type: part.mimeType as 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp' } }); } } else if (part instanceof LanguageModelToolResultPart || part instanceof LanguageModelToolResultPart2) { convertedContent.push({ type: 'tool_result', tool_use_id: part.callId, content: part.content.map((p): TextBlockParam | ImageBlockParam | undefined => { if (p instanceof LanguageModelTextPart) { return { type: 'text', text: p.value }; } else if (p instanceof LanguageModelDataPart && p.mimeType === CustomDataPartMimeTypes.CacheControl && p.data.toString() === 'ephemeral') { // Empty string is invalid return { type: 'text', text: ' ', cache_control: { type: 'ephemeral' } }; } else if (p instanceof LanguageModelDataPart) { return { type: 'image', source: { type: 'base64', media_type: p.mimeType as any, data: Buffer.from(p.data).toString('base64') } }; } }).filter(isDefined), }); } else { // Anthropic errors if we have text parts with empty string text content if (part.value === '') { continue; } convertedContent.push({ type: 'text', text: part.value }); } } return convertedContent; } export function apiMessageToAnthropicMessage(messages: LanguageModelChatMessage[]): { messages: MessageParam[]; system: TextBlockParam } { const unmergedMessages: MessageParam[] = []; const systemMessage: TextBlockParam = { type: 'text', text: '' }; for (const message of messages) { if (message.role === LanguageModelChatMessageRole.Assistant) { unmergedMessages.push({ role: 'assistant', content: apiContentToAnthropicContent(message.content), }); } else if (message.role === LanguageModelChatMessageRole.User) { unmergedMessages.push({ role: 'user', content: apiContentToAnthropicContent(message.content), }); } else { systemMessage.text += message.content.map(p => { // For some reason instance of doesn't work if (p instanceof LanguageModelTextPart) { return p.value; } else if (p instanceof LanguageModelDataPart && p.mimeType === CustomDataPartMimeTypes.CacheControl && p.data.toString() === 'ephemeral') { systemMessage.cache_control = { type: 'ephemeral' }; } return ''; }).join(''); } } // Merge messages of the same type that are adjacent together, this is what anthropic expects const mergedMessages: MessageParam[] = []; for (const message of unmergedMessages) { if (mergedMessages.length === 0 || mergedMessages[mergedMessages.length - 1].role !== message.role) { mergedMessages.push(message); } else { // Merge with the previous message of the same role const prevMessage = mergedMessages[mergedMessages.length - 1]; // Concat the content arrays if they're both arrays - They always will be due to the way apiContentToAnthropicContent works if (Array.isArray(prevMessage.content) && Array.isArray(message.content)) { (prevMessage.content as ContentBlockParam[]).push(...(message.content as ContentBlockParam[])); } } } return { messages: mergedMessages, system: systemMessage }; } function contentBlockSupportsCacheControl(block: ContentBlockParam): block is Exclude { return block.type !== 'thinking' && block.type !== 'redacted_thinking'; } export function anthropicMessagesToRawMessagesForLogging(messages: MessageParam[], system: TextBlockParam): Raw.ChatMessage[] { // Start with full-fidelity conversion, then sanitize for logging const fullMessages = anthropicMessagesToRawMessages(messages, system); // Replace bulky content with placeholders return fullMessages.map(message => { const content = message.content.map(part => { if (part.type === Raw.ChatCompletionContentPartKind.Image) { // Replace actual image URLs with placeholder for logging return { ...part, imageUrl: { url: '(image)' } }; } return part; }); if (message.role === Raw.ChatRole.Tool) { // Replace tool result content with placeholder for logging return { ...message, content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: '(tool result)' }] }; } return { ...message, content }; }); } /** * Full-fidelity conversion of Anthropic MessageParam[] + system to Raw.ChatMessage[] suitable for sending to endpoints. * Compared to the logging variant, this preserves tool_result content and image data (as data URLs when possible). */ export function anthropicMessagesToRawMessages(messages: MessageParam[], system: TextBlockParam): Raw.ChatMessage[] { const rawMessages: Raw.ChatMessage[] = []; if (system) { const systemContent: Raw.ChatCompletionContentPart[] = []; if (system.text) { systemContent.push({ type: Raw.ChatCompletionContentPartKind.Text, text: system.text }); } if (system.cache_control) { systemContent.push({ type: Raw.ChatCompletionContentPartKind.CacheBreakpoint, cacheType: system.cache_control.type }); } if (systemContent.length) { rawMessages.push({ role: Raw.ChatRole.System, content: systemContent }); } } for (const message of messages) { const content: Raw.ChatCompletionContentPart[] = []; let toolCalls: Raw.ChatMessageToolCall[] | undefined; let toolCallId: string | undefined; const toRawImage = (img: ImageBlockParam): Raw.ChatCompletionContentPartImage | undefined => { if (img.source.type === 'base64') { return { type: Raw.ChatCompletionContentPartKind.Image, imageUrl: { url: `data:${img.source.media_type};base64,${img.source.data}` } }; } else if (img.source.type === 'url') { return { type: Raw.ChatCompletionContentPartKind.Image, imageUrl: { url: img.source.url } }; } }; const pushImage = (img: ImageBlockParam) => { const imagePart = toRawImage(img); if (imagePart) { content.push(imagePart); } }; const pushCache = (block?: ContentBlockParam) => { if (block && contentBlockSupportsCacheControl(block) && block.cache_control) { content.push({ type: Raw.ChatCompletionContentPartKind.CacheBreakpoint, cacheType: block.cache_control.type }); } }; if (Array.isArray(message.content)) { for (const block of message.content) { if (block.type === 'text') { content.push({ type: Raw.ChatCompletionContentPartKind.Text, text: block.text }); pushCache(block); } else if (block.type === 'image') { pushImage(block); pushCache(block); } else if (block.type === 'thinking') { // Include thinking content for logging content.push({ type: Raw.ChatCompletionContentPartKind.Text, text: `[THINKING: ${block.thinking}]` }); } else if (block.type === 'redacted_thinking') { content.push({ type: Raw.ChatCompletionContentPartKind.Text, text: '[REDACTED THINKING]' }); } else if (block.type === 'tool_use') { // tool_use appears in assistant messages; represent as toolCalls on assistant message toolCalls ??= []; toolCalls.push({ id: block.id, type: 'function', function: { name: block.name, arguments: JSON.stringify(block.input ?? {}) } }); // no content part, tool call is separate pushCache(block); } else if (block.type === 'tool_result') { // tool_result appears in user role; we'll emit a Raw.Tool message later with this toolCallId and content toolCallId = block.tool_use_id; // Translate tool result content to raw parts const toolContent: Raw.ChatCompletionContentPart[] = []; if (typeof block.content === 'string') { toolContent.push({ type: Raw.ChatCompletionContentPartKind.Text, text: block.content }); } else { for (const c of block.content ?? []) { if (c.type === 'text') { toolContent.push({ type: Raw.ChatCompletionContentPartKind.Text, text: c.text }); } else if (c.type === 'image') { const imagePart = toRawImage(c); if (imagePart) { toolContent.push(imagePart); } } } } // Emit the tool result message now and continue to next message rawMessages.push({ role: Raw.ChatRole.Tool, content: toolContent.length ? toolContent : [{ type: Raw.ChatCompletionContentPartKind.Text, text: '' }], toolCallId }); toolCallId = undefined; } else { // thinking or unsupported types are ignored } } } else if (typeof message.content === 'string') { content.push({ type: Raw.ChatCompletionContentPartKind.Text, text: message.content }); } if (message.role === 'assistant') { const msg: Raw.AssistantChatMessage = { role: Raw.ChatRole.Assistant, content }; if (toolCalls && toolCalls.length > 0) { msg.toolCalls = toolCalls; } rawMessages.push(msg); } else if (message.role === 'user') { // note: tool_result handled earlier; here we push standard user content if any if (content.length) { rawMessages.push({ role: Raw.ChatRole.User, content }); } } } return rawMessages; } ================================================ FILE: src/extension/byok/common/byokProvider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import type { Disposable, LanguageModelChatInformation, LanguageModelDataPart, LanguageModelTextPart, LanguageModelThinkingPart, LanguageModelToolCallPart, LanguageModelToolResultPart } from 'vscode'; import { CopilotToken } from '../../../platform/authentication/common/copilotToken'; import { ICAPIClientService } from '../../../platform/endpoint/common/capiClient'; import { EndpointEditToolName, IChatModelInformation, ModelSupportedEndpoint } from '../../../platform/endpoint/common/endpointProvider'; import { isScenarioAutomation } from '../../../platform/env/common/envService'; import { TokenizerType } from '../../../util/common/tokenizer'; export const enum BYOKAuthType { /** * Requires a single API key for all models (e.g., OpenAI) */ GlobalApiKey, /** * Requires both deployment URL and API key per model (e.g., Azure) */ PerModelDeployment, /** * No authentication required (e.g., Ollama) */ None } interface BYOKBaseModelConfig { modelId: string; capabilities?: BYOKModelCapabilities; } export type LMResponsePart = LanguageModelTextPart | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelThinkingPart | LanguageModelToolResultPart; export interface BYOKGlobalKeyModelConfig extends BYOKBaseModelConfig { apiKey: string; } export interface BYOKPerModelConfig extends BYOKBaseModelConfig { apiKey: string; deploymentUrl: string; } interface BYOKNoAuthModelConfig extends BYOKBaseModelConfig { // No additional fields required } export type BYOKModelConfig = BYOKGlobalKeyModelConfig | BYOKPerModelConfig | BYOKNoAuthModelConfig; export interface BYOKModelCapabilities { name: string; url?: string; maxInputTokens: number; maxOutputTokens: number; toolCalling: boolean; vision: boolean; thinking?: boolean; adaptiveThinking?: boolean; streaming?: boolean; editTools?: EndpointEditToolName[]; requestHeaders?: Record; supportedEndpoints?: ModelSupportedEndpoint[]; zeroDataRetentionEnabled?: boolean; } export interface BYOKModelRegistry { readonly name: string; readonly authType: BYOKAuthType; updateKnownModelsList(knownModels: BYOKKnownModels | undefined): void; getAllModels(apiKey?: string): Promise<{ id: string; name: string }[]>; registerModel(config: BYOKModelConfig): Promise; } // Many model providers don't have robust model lists. This allows us to map id -> information about models, and then if we don't know the model just let the user enter a custom id export type BYOKKnownModels = Record; // Type guards to ensure correct config type export function isGlobalKeyConfig(config: BYOKModelConfig): config is BYOKGlobalKeyModelConfig { return 'apiKey' in config && !('deploymentUrl' in config); } export function isPerModelConfig(config: BYOKModelConfig): config is BYOKPerModelConfig { return 'apiKey' in config && 'deploymentUrl' in config; } export function isNoAuthConfig(config: BYOKModelConfig): config is BYOKNoAuthModelConfig { return !('apiKey' in config) && !('deploymentUrl' in config); } export function resolveModelInfo(modelId: string, providerName: string, knownModels: BYOKKnownModels | undefined, modelCapabilities?: BYOKModelCapabilities): IChatModelInformation { // Model Capabilities are something the user has decided on so those take precedence, then we rely on known model info, then defaults. let knownModelInfo = modelCapabilities; if (knownModels && !knownModelInfo) { knownModelInfo = knownModels[modelId]; } const modelName = knownModelInfo?.name || modelId; const contextWinow = knownModelInfo ? (knownModelInfo.maxInputTokens + knownModelInfo.maxOutputTokens) : 128000; const modelInfo: IChatModelInformation = { id: modelId, name: modelName, vendor: providerName, version: '1.0.0', capabilities: { type: 'chat', family: modelId, supports: { streaming: knownModelInfo?.streaming ?? true, tool_calls: !!knownModelInfo?.toolCalling, vision: !!knownModelInfo?.vision, thinking: !!knownModelInfo?.thinking, adaptive_thinking: !!knownModelInfo?.adaptiveThinking }, tokenizer: TokenizerType.O200K, limits: { max_context_window_tokens: contextWinow, max_prompt_tokens: knownModelInfo?.maxInputTokens || 100000, max_output_tokens: knownModelInfo?.maxOutputTokens || 8192 } }, is_chat_default: false, is_chat_fallback: false, model_picker_enabled: true, supported_endpoints: knownModelInfo?.supportedEndpoints, zeroDataRetentionEnabled: knownModelInfo?.zeroDataRetentionEnabled }; if (knownModelInfo?.requestHeaders && Object.keys(knownModelInfo.requestHeaders).length > 0) { modelInfo.requestHeaders = { ...knownModelInfo.requestHeaders }; } return modelInfo; } export function byokKnownModelsToAPIInfo(providerName: string, knownModels: BYOKKnownModels | undefined): LanguageModelChatInformation[] { if (!knownModels) { return []; } return Object.entries(knownModels).map(([id, capabilities]) => byokKnownModelToAPIInfo(providerName, id, capabilities)); } export function byokKnownModelToAPIInfo(providerName: string, id: string, capabilities: BYOKModelCapabilities): LanguageModelChatInformation { return { id, name: capabilities.name, version: '1.0.0', maxOutputTokens: capabilities.maxOutputTokens, maxInputTokens: capabilities.maxInputTokens, detail: providerName, family: id, tooltip: `${capabilities.name} is contributed via the ${providerName} provider.`, multiplierNumeric: 0, capabilities: { toolCalling: capabilities.toolCalling, imageInput: capabilities.vision } }; } export function isBYOKEnabled(copilotToken: Omit, capiClientService: ICAPIClientService): boolean { if (isScenarioAutomation) { return true; } const isGHE = capiClientService.dotcomAPIURL !== 'https://api.github.com'; const byokAllowed = (copilotToken.isInternal || copilotToken.isIndividual) && !isGHE; return byokAllowed; } /** * Result of handling an API key update operation. */ export interface HandleAPIKeyUpdateResult { /** * The new API key value, or undefined if the key was deleted or operation was cancelled. */ apiKey: string | undefined; /** * Whether the API key was deleted (user entered empty string during reconfigure). */ deleted: boolean; /** * Whether the operation was cancelled (user dismissed the input). */ cancelled: boolean; } /** * Storage service interface for BYOK API key operations. * This is a minimal interface to avoid importing the full IBYOKStorageService in common code. */ export interface IBYOKStorageServiceLike { getAPIKey(providerName: string, modelId?: string): Promise; storeAPIKey(providerName: string, apiKey: string, authType: BYOKAuthType, modelId?: string): Promise; deleteAPIKey(providerName: string, authType: BYOKAuthType, modelId?: string): Promise; } /** * Handles API key update flow for BYOK providers using a consistent pattern. * This utility handles all three cases from promptForAPIKey: * - undefined: user cancelled/dismissed the input * - empty string: user wants to delete the saved key (only when reconfiguring) * - non-empty string: user provided a new API key * * @param providerName - Name of the provider (e.g., 'Anthropic', 'Gemini') * @param storageService - Storage service for API key operations * @param promptForAPIKeyFn - Function to prompt user for API key * @returns Result containing the new API key (if any) and status flags */ export async function handleAPIKeyUpdate( providerName: string, storageService: IBYOKStorageServiceLike, promptForAPIKeyFn: (providerName: string, reconfigure: boolean) => Promise ): Promise { const existingKey = await storageService.getAPIKey(providerName); const isReconfiguring = existingKey !== undefined; const newAPIKey = await promptForAPIKeyFn(providerName, isReconfiguring); if (newAPIKey === undefined) { // User cancelled/dismissed the input return { apiKey: undefined, deleted: false, cancelled: true }; } else if (newAPIKey === '') { // User wants to delete the key (only valid when reconfiguring) await storageService.deleteAPIKey(providerName, BYOKAuthType.GlobalApiKey); return { apiKey: undefined, deleted: true, cancelled: false }; } else { // User provided a new API key await storageService.storeAPIKey(providerName, newAPIKey, BYOKAuthType.GlobalApiKey); return { apiKey: newAPIKey, deleted: false, cancelled: false }; } } ================================================ FILE: src/extension/byok/common/geminiFunctionDeclarationConverter.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { FunctionDeclaration, Schema, Type } from '@google/genai'; export type ToolJsonSchema = { type?: string; description?: string; properties?: Record; items?: ToolJsonSchema; required?: string[]; enum?: string[]; // Add support for JSON Schema composition keywords anyOf?: ToolJsonSchema[]; oneOf?: ToolJsonSchema[]; allOf?: ToolJsonSchema[]; }; // Map JSON schema types to Gemini Type enum function mapType(jsonType: string): Type { switch (jsonType) { case 'object': return Type.OBJECT; case 'array': return Type.ARRAY; case 'string': return Type.STRING; case 'number': return Type.NUMBER; case 'integer': return Type.INTEGER; case 'boolean': return Type.BOOLEAN; case 'null': return Type.NULL; default: throw new Error(`Unsupported type: ${jsonType}`); } } // Convert JSON schema → Gemini function declaration export function toGeminiFunction(name: string, description: string, schema: ToolJsonSchema): FunctionDeclaration { // If schema root is array, we use its items for function parameters const target = schema.type === 'array' && schema.items ? schema.items : schema; const parameters: Schema = { type: Type.OBJECT, properties: transformProperties(target.properties || {}), required: Array.isArray(target.required) ? target.required : [] }; return { name, description: description || 'No description provided.', parameters }; } // Recursive transformation for nested properties function transformProperties(props: Record): Record { const result: Record = {}; for (const [key, value] of Object.entries(props)) { // Handle anyOf, oneOf, allOf by picking the first valid entry const effectiveValue = (value.anyOf?.[0] || value.oneOf?.[0] || value.allOf?.[0] || value) as ToolJsonSchema; const transformed: any = { // If type is undefined, throw an error to avoid incorrect assumptions type: effectiveValue.type ? mapType(effectiveValue.type) : Type.OBJECT }; if (effectiveValue.description) { transformed.description = effectiveValue.description; } // Enum support if (effectiveValue.enum) { transformed.enum = effectiveValue.enum; } if (effectiveValue.type === 'object' && effectiveValue.properties) { transformed.properties = transformProperties(effectiveValue.properties); if (effectiveValue.required) { transformed.required = effectiveValue.required; } } else if (effectiveValue.type === 'array' && effectiveValue.items) { const itemType = effectiveValue.items.type === 'object' ? Type.OBJECT : mapType(effectiveValue.items.type ?? 'object'); const itemSchema: any = { type: itemType }; if (effectiveValue.items.description) { itemSchema.description = effectiveValue.items.description; } if (effectiveValue.items.enum) { itemSchema.enum = effectiveValue.items.enum; } if (effectiveValue.items.properties) { itemSchema.properties = transformProperties(effectiveValue.items.properties); if (effectiveValue.items.required) { itemSchema.required = effectiveValue.items.required; } } transformed.items = itemSchema; } result[key] = transformed; } return result; } ================================================ FILE: src/extension/byok/common/geminiMessageConverter.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import type { Content, FunctionCall, FunctionResponse, Part } from '@google/genai'; import { Raw } from '@vscode/prompt-tsx'; import type { LanguageModelChatMessage } from 'vscode'; import { CustomDataPartMimeTypes } from '../../../platform/endpoint/common/endpointTypes'; import { LanguageModelChatMessageRole, LanguageModelDataPart, LanguageModelTextPart, LanguageModelThinkingPart, LanguageModelToolCallPart, LanguageModelToolResultPart, LanguageModelToolResultPart2 } from '../../../vscodeTypes'; function apiContentToGeminiContent(content: (LanguageModelTextPart | LanguageModelToolResultPart | LanguageModelToolCallPart | LanguageModelDataPart | LanguageModelThinkingPart)[]): Part[] { const convertedContent: Part[] = []; let pendingSignature: string | undefined; for (const part of content) { if (part instanceof LanguageModelThinkingPart) { // Extract thought signature from thinking part metadata if (part.metadata && typeof part.metadata === 'object' && 'signature' in part.metadata) { const metadataObj = part.metadata as Record; if (typeof metadataObj.signature === 'string') { pendingSignature = metadataObj.signature; } } // Note: We don't emit thinking content to Gemini as it's already been processed // The signature will be attached to the next function call } else if (part instanceof LanguageModelToolCallPart) { const functionCallPart: Part = { functionCall: { name: part.name, args: part.input as Record || {} }, // Attach pending thought signature if available (required by Gemini 3 for function calling) ...(pendingSignature ? { thoughtSignature: pendingSignature } : {}) }; if (pendingSignature) { pendingSignature = undefined; // Clear after use } convertedContent.push(functionCallPart); } else if (part instanceof LanguageModelDataPart) { if (part.mimeType !== CustomDataPartMimeTypes.StatefulMarker && part.mimeType !== CustomDataPartMimeTypes.CacheControl) { convertedContent.push({ inlineData: { data: Buffer.from(part.data).toString('base64'), mimeType: part.mimeType } }); } } else if (part instanceof LanguageModelToolResultPart || part instanceof LanguageModelToolResultPart2) { // Convert tool result content - handle both text and image parts const textContent = part.content .filter((p): p is LanguageModelTextPart => p instanceof LanguageModelTextPart) .map(p => p.value) .join(''); // Handle image parts in tool results const imageParts = part.content.filter((p): p is LanguageModelDataPart => p instanceof LanguageModelDataPart && p.mimeType !== CustomDataPartMimeTypes.StatefulMarker && p.mimeType !== CustomDataPartMimeTypes.CacheControl ); // If there are images, we need to handle them differently // For now, we'll include image info in the text response since Gemini function responses expect structured data let imageDescription = ''; if (imageParts.length > 0) { imageDescription = `\n[Contains ${imageParts.length} image(s) with types: ${imageParts.map(p => p.mimeType).join(', ')}]`; } // extraction: functionName_timestamp => split on first underscore const functionName = part.callId?.split('_')[0] || 'unknown_function'; // Preserve structured JSON if possible let responsePayload: any = {}; if (textContent) { // Handle case with text content (may also have images) try { responsePayload = JSON.parse(textContent); if (typeof responsePayload !== 'object' || responsePayload === null || Array.isArray(responsePayload)) { responsePayload = { result: responsePayload }; } } catch { responsePayload = { result: textContent + imageDescription }; } // Add image info if present if (imageParts.length > 0) { responsePayload.images = imageParts.map(p => ({ mimeType: p.mimeType, size: p.data.length, data: Buffer.from(p.data).toString('base64') })); } } else if (imageParts.length > 0) { // Only images, no text content responsePayload = { images: imageParts.map(p => ({ mimeType: p.mimeType, size: p.data.length, data: Buffer.from(p.data).toString('base64') })) }; } const functionResponse: FunctionResponse = { name: functionName, response: responsePayload }; convertedContent.push({ functionResponse }); } else if (part instanceof LanguageModelTextPart) { // Text content - only filter completely empty strings, keep whitespace if (part.value !== '') { convertedContent.push({ text: part.value }); } } } return convertedContent; } export function apiMessageToGeminiMessage(messages: LanguageModelChatMessage[]): { contents: Content[]; systemInstruction?: Content } { const contents: Content[] = []; let systemInstruction: Content | undefined; // Track tool calls to match with their responses const pendingToolCalls = new Map(); for (const message of messages) { if (message.role === LanguageModelChatMessageRole.System) { // Gemini uses system instruction separately const systemText = message.content .filter((p): p is LanguageModelTextPart => p instanceof LanguageModelTextPart) .map(p => p.value) .join(''); if (systemText.trim()) { systemInstruction = { role: 'user', parts: [{ text: systemText }] }; } } else if (message.role === LanguageModelChatMessageRole.Assistant) { const parts = apiContentToGeminiContent(message.content); // Store function calls for later matching with responses parts.forEach(part => { if (part.functionCall && part.functionCall.name) { pendingToolCalls.set(part.functionCall.name, part.functionCall); } }); contents.push({ role: 'model', parts }); } else if (message.role === LanguageModelChatMessageRole.User) { const parts = apiContentToGeminiContent(message.content); contents.push({ role: 'user', parts }); } } // Post-process: ensure functionResponse parts are not embedded in 'model' role messages. // Gemini expects tool responses to be supplied by the *user*/caller after the model issues a functionCall. // If upstream accidentally placed tool result parts inside an assistant/model role, we split them out here. for (let i = 0; i < contents.length; i++) { const c = contents[i]; if (c.role === 'model' && c.parts && c.parts.some(p => 'functionResponse' in p)) { const modelParts: Part[] = []; const toolResultParts: Part[] = []; for (const p of c.parts) { if ('functionResponse' in p) { toolResultParts.push(p); } else { modelParts.push(p); } } // Replace original with model-only parts c.parts = modelParts; // Insert a new user role content immediately after with the function responses if (toolResultParts.length) { contents.splice(i + 1, 0, { role: 'user', parts: toolResultParts }); i++; // Skip over inserted element } } } // Cleanup: remove any model messages that became empty after extraction for (let i = contents.length - 1; i >= 0; i--) { const c = contents[i]; if (c.role === 'model' && (!c.parts || c.parts.length === 0)) { contents.splice(i, 1); } } return { contents, systemInstruction }; } export function geminiMessagesToRawMessagesForLogging(contents: Content[], systemInstruction?: Content): Raw.ChatMessage[] { const fullMessages = geminiMessagesToRawMessages(contents, systemInstruction); // Replace bulky content with placeholders for logging return fullMessages.map(message => { const content = message.content.map(part => { if (part.type === Raw.ChatCompletionContentPartKind.Image) { return { ...part, imageUrl: { url: '(image)' } }; } return part; }); if (message.role === Raw.ChatRole.Tool) { return { ...message, content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: '(tool result)' }] }; } return { ...message, content }; }); } export function geminiMessagesToRawMessages(contents: Content[], systemInstruction?: Content): Raw.ChatMessage[] { const rawMessages: Raw.ChatMessage[] = []; // Add system instruction if present if (systemInstruction && systemInstruction.parts) { const systemContent: Raw.ChatCompletionContentPart[] = []; systemInstruction.parts.forEach((part: Part) => { if (part.text) { systemContent.push({ type: Raw.ChatCompletionContentPartKind.Text, text: part.text }); } }); if (systemContent.length) { rawMessages.push({ role: Raw.ChatRole.System, content: systemContent }); } } // Convert Gemini contents to raw messages for (const content of contents) { const messageParts: Raw.ChatCompletionContentPart[] = []; let toolCalls: Raw.ChatMessageToolCall[] | undefined; if (content.parts) { content.parts.forEach((part: Part) => { if (part.text) { messageParts.push({ type: Raw.ChatCompletionContentPartKind.Text, text: part.text }); } else if (part.inlineData) { messageParts.push({ type: Raw.ChatCompletionContentPartKind.Image, imageUrl: { url: `data:${part.inlineData.mimeType};base64,${part.inlineData.data}` } }); } else if (part.functionCall && part.functionCall.name) { toolCalls ??= []; toolCalls.push({ id: part.functionCall.name, // Gemini doesn't have call IDs, use name type: 'function', function: { name: part.functionCall.name, arguments: JSON.stringify(part.functionCall.args ?? {}) } }); } else if (part.functionResponse && part.functionResponse.name) { // Function responses should be emitted as tool messages const toolContent: Raw.ChatCompletionContentPart[] = []; // Handle structured response that might contain image data const response = part.functionResponse.response; if (response && typeof response === 'object' && 'images' in response && Array.isArray(response.images)) { // Extract images from structured response and convert to Raw format for (const img of response.images) { if (img && typeof img === 'object' && 'data' in img && 'mimeType' in img) { toolContent.push({ type: Raw.ChatCompletionContentPartKind.Image, imageUrl: { url: `data:${img.mimeType};base64,${img.data}` } }); } } // Create a clean response object without the raw image data for text content const cleanResponse = { ...response }; if ('images' in cleanResponse) { cleanResponse.images = response.images.map((img: any) => ({ mimeType: img.mimeType, size: img.size || (img.data ? img.data.length : 0) })); } toolContent.push({ type: Raw.ChatCompletionContentPartKind.Text, text: JSON.stringify(cleanResponse) }); } else { // Standard text-only response toolContent.push({ type: Raw.ChatCompletionContentPartKind.Text, text: JSON.stringify(response) }); } rawMessages.push({ role: Raw.ChatRole.Tool, content: toolContent, toolCallId: part.functionResponse.name }); } }); } // Add the main message if it has content if (messageParts.length > 0 || toolCalls) { const role = content.role === 'model' ? Raw.ChatRole.Assistant : Raw.ChatRole.User; const msg: Raw.ChatMessage = { role, content: messageParts }; if (toolCalls && content.role === 'model') { (msg as Raw.AssistantChatMessage).toolCalls = toolCalls; } rawMessages.push(msg); } } return rawMessages; } ================================================ FILE: src/extension/byok/common/test/__snapshots__/anthropicMessageConverter.spec.ts.snap ================================================ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`anthropicMessagesToRawMessages > converts messages with content blocks 1`] = ` [ { "content": [ { "text": "System prompt", "type": 1, }, ], "role": 0, }, { "content": [ { "text": "Look at this image:", "type": 1, }, { "imageUrl": { "url": "data:image/jpeg;base64,fake-base64-data", }, "type": 0, }, ], "role": 1, }, ] `; exports[`anthropicMessagesToRawMessages > converts simple text messages 1`] = ` [ { "content": [ { "text": "You are a helpful assistant", "type": 1, }, ], "role": 0, }, { "content": [ { "text": "Hello world", "type": 1, }, ], "role": 1, }, { "content": [ { "text": "Hi there!", "type": 1, }, ], "role": 2, }, ] `; exports[`anthropicMessagesToRawMessages > converts tool result messages 1`] = ` [ { "content": [ { "text": "The weather in London is sunny", "type": 1, }, ], "role": 3, "toolCallId": "call_123", }, ] `; exports[`anthropicMessagesToRawMessages > converts tool result with content blocks 1`] = ` [ { "content": [ { "text": "Here is the chart:", "type": 1, }, { "imageUrl": { "url": "data:image/png;base64,chart-data", }, "type": 0, }, ], "role": 3, "toolCallId": "call_456", }, ] `; exports[`anthropicMessagesToRawMessages > converts tool use messages 1`] = ` [ { "content": [ { "text": "I will use a tool:", "type": 1, }, ], "role": 2, "toolCalls": [ { "function": { "arguments": "{"location":"London"}", "name": "get_weather", }, "id": "call_123", "type": "function", }, ], }, ] `; exports[`anthropicMessagesToRawMessages > handles cache control blocks 1`] = ` [ { "content": [ { "text": "System with cache", "type": 1, }, { "cacheType": "ephemeral", "type": 3, }, ], "role": 0, }, { "content": [ { "text": "Cached content", "type": 1, }, { "cacheType": "ephemeral", "type": 3, }, ], "role": 1, }, ] `; exports[`anthropicMessagesToRawMessages > handles empty system message 1`] = ` [ { "content": [ { "text": "Hello", "type": 1, }, ], "role": 1, }, ] `; exports[`anthropicMessagesToRawMessages > handles empty tool result content 1`] = ` [ { "content": [ { "text": "", "type": 1, }, ], "role": 3, "toolCallId": "call_empty", }, ] `; exports[`anthropicMessagesToRawMessages > handles url-based images 1`] = ` [ { "content": [ { "imageUrl": { "url": "https://example.com/image.jpg", }, "type": 0, }, ], "role": 1, }, ] `; exports[`anthropicMessagesToRawMessages > includes thinking blocks in conversion to raw messages 1`] = ` [ { "content": [ { "text": "[THINKING: Let me think...]", "type": 1, }, { "text": "Here is my response", "type": 1, }, ], "role": 2, }, ] `; ================================================ FILE: src/extension/byok/common/test/anthropicMessageConverter.spec.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { MessageParam, TextBlockParam } from '@anthropic-ai/sdk/resources'; import { expect, suite, test } from 'vitest'; import { anthropicMessagesToRawMessages } from '../anthropicMessageConverter'; suite('anthropicMessagesToRawMessages', function () { test('converts simple text messages', function () { const messages: MessageParam[] = [ { role: 'user', content: 'Hello world' }, { role: 'assistant', content: 'Hi there!' } ]; const system: TextBlockParam = { type: 'text', text: 'You are a helpful assistant' }; const result = anthropicMessagesToRawMessages(messages, system); expect(result).toMatchSnapshot(); }); test('handles empty system message', function () { const messages: MessageParam[] = [ { role: 'user', content: 'Hello' } ]; const system: TextBlockParam = { type: 'text', text: '' }; const result = anthropicMessagesToRawMessages(messages, system); expect(result).toMatchSnapshot(); }); test('converts messages with content blocks', function () { const messages: MessageParam[] = [ { role: 'user', content: [ { type: 'text', text: 'Look at this image:' }, { type: 'image', source: { type: 'base64', media_type: 'image/jpeg', data: 'fake-base64-data' } } ] } ]; const system: TextBlockParam = { type: 'text', text: 'System prompt' }; const result = anthropicMessagesToRawMessages(messages, system); expect(result).toMatchSnapshot(); }); test('converts tool use messages', function () { const messages: MessageParam[] = [ { role: 'assistant', content: [ { type: 'text', text: 'I will use a tool:' }, { type: 'tool_use', id: 'call_123', name: 'get_weather', input: { location: 'London' } } ] } ]; const system: TextBlockParam = { type: 'text', text: '' }; const result = anthropicMessagesToRawMessages(messages, system); expect(result).toMatchSnapshot(); }); test('converts tool result messages', function () { const messages: MessageParam[] = [ { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'call_123', content: 'The weather in London is sunny' } ] } ]; const system: TextBlockParam = { type: 'text', text: '' }; const result = anthropicMessagesToRawMessages(messages, system); expect(result).toMatchSnapshot(); }); test('converts tool result with content blocks', function () { const messages: MessageParam[] = [ { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'call_456', content: [ { type: 'text', text: 'Here is the chart:' }, { type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'chart-data' } } ] } ] } ]; const system: TextBlockParam = { type: 'text', text: '' }; const result = anthropicMessagesToRawMessages(messages, system); expect(result).toMatchSnapshot(); }); test('handles cache control blocks', function () { const messages: MessageParam[] = [ { role: 'user', content: [ { type: 'text', text: 'Cached content', cache_control: { type: 'ephemeral' } } ] } ]; const system: TextBlockParam = { type: 'text', text: 'System with cache', cache_control: { type: 'ephemeral' } }; const result = anthropicMessagesToRawMessages(messages, system); expect(result).toMatchSnapshot(); }); test('includes thinking blocks in conversion to raw messages', function () { const messages: MessageParam[] = [ { role: 'assistant', content: [ { type: 'thinking', thinking: 'Let me think...', signature: '' }, { type: 'text', text: 'Here is my response' } ] } ]; const system: TextBlockParam = { type: 'text', text: '' }; const result = anthropicMessagesToRawMessages(messages, system); expect(result).toMatchSnapshot(); }); test('handles url-based images', function () { const messages: MessageParam[] = [ { role: 'user', content: [ { type: 'image', source: { type: 'url', url: 'https://example.com/image.jpg' } } ] } ]; const system: TextBlockParam = { type: 'text', text: '' }; const result = anthropicMessagesToRawMessages(messages, system); expect(result).toMatchSnapshot(); }); test('handles empty tool result content', function () { const messages: MessageParam[] = [ { role: 'user', content: [ { type: 'tool_result', tool_use_id: 'call_empty', content: [] } ] } ]; const system: TextBlockParam = { type: 'text', text: '' }; const result = anthropicMessagesToRawMessages(messages, system); expect(result).toMatchSnapshot(); }); }); ================================================ FILE: src/extension/byok/common/test/geminiFunctionDeclarationConverter.spec.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Type } from '@google/genai'; import { describe, expect, it } from 'vitest'; import { toGeminiFunction, ToolJsonSchema } from '../geminiFunctionDeclarationConverter'; describe('GeminiFunctionDeclarationConverter', () => { describe('toGeminiFunction', () => { it('should convert basic function with simple parameters', () => { const schema: ToolJsonSchema = { type: 'object', properties: { name: { type: 'string', description: 'The name parameter' }, age: { type: 'number', description: 'The age parameter' }, isActive: { type: 'boolean', description: 'Whether the user is active' } }, required: ['name', 'age'] }; const result = toGeminiFunction('testFunction', 'A test function', schema); expect(result.name).toBe('testFunction'); expect(result.description).toBe('A test function'); expect(result.parameters).toBeDefined(); expect(result.parameters!.type).toBe(Type.OBJECT); expect(result.parameters!.required).toEqual(['name', 'age']); expect(result.parameters!.properties).toBeDefined(); expect(result.parameters!.properties!['name']).toEqual({ type: Type.STRING, description: 'The name parameter' }); expect(result.parameters!.properties!['age']).toEqual({ type: Type.NUMBER, description: 'The age parameter' }); expect(result.parameters!.properties!['isActive']).toEqual({ type: Type.BOOLEAN, description: 'Whether the user is active' }); }); it('should handle function with no description', () => { const schema: ToolJsonSchema = { type: 'object', properties: { value: { type: 'string' } } }; const result = toGeminiFunction('noDescFunction', '', schema); expect(result.description).toBe('No description provided.'); }); it('should handle integer type by mapping to INTEGER', () => { const schema: ToolJsonSchema = { type: 'object', properties: { count: { type: 'integer', description: 'An integer count' }, groupIndex: { type: 'integer', description: 'Group index' } }, required: ['count'] }; const result = toGeminiFunction('integerFunction', 'Function with integer parameters', schema); expect(result.parameters).toBeDefined(); expect(result.parameters!.type).toBe(Type.OBJECT); expect(result.parameters!.required).toEqual(['count']); expect(result.parameters!.properties).toBeDefined(); expect(result.parameters!.properties!['count']).toEqual({ type: Type.INTEGER, description: 'An integer count' }); expect(result.parameters!.properties!['groupIndex']).toEqual({ type: Type.INTEGER, description: 'Group index' }); }); it('should handle null type by mapping to NULL', () => { const schema: ToolJsonSchema = { type: 'object', properties: { nullableField: { type: 'null', description: 'A nullable field' } } }; const result = toGeminiFunction('nullFunction', 'Function with null parameter', schema); expect(result.parameters).toBeDefined(); expect(result.parameters!.properties).toBeDefined(); expect(result.parameters!.properties!['nullableField']).toEqual({ type: Type.NULL, description: 'A nullable field' }); }); it('should handle array schema by using items as parameters', () => { const schema: ToolJsonSchema = { type: 'array', items: { type: 'object', properties: { id: { type: 'string' }, count: { type: 'number' } }, required: ['id'] } }; const result = toGeminiFunction('arrayFunction', 'Array function', schema); expect(result.parameters).toBeDefined(); expect(result.parameters!.type).toBe(Type.OBJECT); expect(result.parameters!.required).toEqual(['id']); expect(result.parameters!.properties).toBeDefined(); expect(result.parameters!.properties!['id']).toEqual({ type: Type.STRING }); expect(result.parameters!.properties!['count']).toEqual({ type: Type.NUMBER }); }); it('should handle nested object properties', () => { const schema: ToolJsonSchema = { type: 'object', properties: { user: { type: 'object', description: 'User information', properties: { profile: { type: 'object', properties: { firstName: { type: 'string' }, lastName: { type: 'string' } }, required: ['firstName'] }, settings: { type: 'object', properties: { theme: { type: 'string' }, notifications: { type: 'boolean' } } } }, required: ['profile'] } } }; const result = toGeminiFunction('nestedFunction', 'Function with nested objects', schema); expect(result.parameters).toBeDefined(); expect(result.parameters!.properties).toBeDefined(); const userProperty = result.parameters!.properties!['user']; expect(userProperty.type).toBe(Type.OBJECT); expect(userProperty.description).toBe('User information'); expect(userProperty.required).toEqual(['profile']); expect(userProperty.properties).toBeDefined(); const profileProperty = userProperty.properties!['profile']; expect(profileProperty.type).toBe(Type.OBJECT); expect(profileProperty.required).toEqual(['firstName']); expect(profileProperty.properties).toBeDefined(); expect(profileProperty.properties!['firstName']).toEqual({ type: Type.STRING }); expect(profileProperty.properties!['lastName']).toEqual({ type: Type.STRING }); const settingsProperty = userProperty.properties!['settings']; expect(settingsProperty.type).toBe(Type.OBJECT); expect(settingsProperty.properties).toBeDefined(); expect(settingsProperty.properties!['theme']).toEqual({ type: Type.STRING }); expect(settingsProperty.properties!['notifications']).toEqual({ type: Type.BOOLEAN }); }); it('should handle array properties with primitive items', () => { const schema: ToolJsonSchema = { type: 'object', properties: { tags: { type: 'array', description: 'List of tags', items: { type: 'string', description: 'Individual tag' } }, scores: { type: 'array', items: { type: 'number' } } } }; const result = toGeminiFunction('arrayPropsFunction', 'Function with arrays', schema); expect(result.parameters).toBeDefined(); expect(result.parameters!.properties).toBeDefined(); const tagsProperty = result.parameters!.properties!['tags']; expect(tagsProperty.type).toBe(Type.ARRAY); expect(tagsProperty.description).toBe('List of tags'); expect(tagsProperty.items).toEqual({ type: Type.STRING, description: 'Individual tag' }); const scoresProperty = result.parameters!.properties!['scores']; expect(scoresProperty.type).toBe(Type.ARRAY); expect(scoresProperty.items).toEqual({ type: Type.NUMBER }); }); it('should handle array properties with object items', () => { const schema: ToolJsonSchema = { type: 'object', properties: { items: { type: 'array', description: 'List of items', items: { type: 'object', description: 'Individual item', properties: { id: { type: 'string' }, name: { type: 'string' }, metadata: { type: 'object', properties: { created: { type: 'string' }, version: { type: 'number' } } } }, required: ['id', 'name'] } } } }; const result = toGeminiFunction('complexArrayFunction', 'Function with complex arrays', schema); expect(result.parameters).toBeDefined(); expect(result.parameters!.properties).toBeDefined(); const itemsProperty = result.parameters!.properties!['items']; expect(itemsProperty.type).toBe(Type.ARRAY); expect(itemsProperty.description).toBe('List of items'); expect(itemsProperty.items).toBeDefined(); expect(itemsProperty.items!.type).toBe(Type.OBJECT); expect(itemsProperty.items!.description).toBe('Individual item'); expect(itemsProperty.items!.required).toEqual(['id', 'name']); expect(itemsProperty.items!.properties).toBeDefined(); expect(itemsProperty.items!.properties!['id']).toEqual({ type: Type.STRING }); expect(itemsProperty.items!.properties!['name']).toEqual({ type: Type.STRING }); expect(itemsProperty.items!.properties!['metadata'].type).toBe(Type.OBJECT); expect(itemsProperty.items!.properties!['metadata'].properties).toBeDefined(); expect(itemsProperty.items!.properties!['metadata'].properties!['created']).toEqual({ type: Type.STRING }); expect(itemsProperty.items!.properties!['metadata'].properties!['version']).toEqual({ type: Type.NUMBER }); }); it('should handle enum properties', () => { const schema: ToolJsonSchema = { type: 'object', properties: { status: { type: 'string', description: 'Status value', enum: ['active', 'inactive', 'pending'] }, priority: { type: 'string', enum: ['1', '2', '3', '4', '5'] } } }; const result = toGeminiFunction('enumFunction', 'Function with enums', schema); expect(result.parameters).toBeDefined(); expect(result.parameters!.properties).toBeDefined(); const statusProperty = result.parameters!.properties!['status']; expect(statusProperty.type).toBe(Type.STRING); expect(statusProperty.description).toBe('Status value'); expect(statusProperty.enum).toEqual(['active', 'inactive', 'pending']); const priorityProperty = result.parameters!.properties!['priority']; expect(priorityProperty.type).toBe(Type.STRING); expect(priorityProperty.enum).toEqual(['1', '2', '3', '4', '5']); }); it('should handle anyOf composition by using first option', () => { const schema: ToolJsonSchema = { type: 'object', properties: { value: { anyOf: [ { type: 'string', description: 'String value' }, { type: 'number', description: 'Number value' } ] } } }; const result = toGeminiFunction('anyOfFunction', 'Function with anyOf', schema); expect(result.parameters).toBeDefined(); expect(result.parameters!.properties).toBeDefined(); const valueProperty = result.parameters!.properties!['value']; expect(valueProperty.type).toBe(Type.STRING); expect(valueProperty.description).toBe('String value'); }); it('should handle oneOf composition by using first option', () => { const schema: ToolJsonSchema = { type: 'object', properties: { data: { oneOf: [ { type: 'boolean', description: 'Boolean data' }, { type: 'string', description: 'String data' } ] } } }; const result = toGeminiFunction('oneOfFunction', 'Function with oneOf', schema); expect(result.parameters).toBeDefined(); expect(result.parameters!.properties).toBeDefined(); const dataProperty = result.parameters!.properties!['data']; expect(dataProperty.type).toBe(Type.BOOLEAN); expect(dataProperty.description).toBe('Boolean data'); }); it('should handle allOf composition by using first option', () => { const schema: ToolJsonSchema = { type: 'object', properties: { config: { allOf: [ { type: 'object', description: 'Config object' }, { type: 'string', description: 'Config string' } ] } } }; const result = toGeminiFunction('allOfFunction', 'Function with allOf', schema); expect(result.parameters).toBeDefined(); expect(result.parameters!.properties).toBeDefined(); const configProperty = result.parameters!.properties!['config']; expect(configProperty.type).toBe(Type.OBJECT); expect(configProperty.description).toBe('Config object'); }); it('should handle schema with no properties', () => { const schema: ToolJsonSchema = { type: 'object' }; const result = toGeminiFunction('emptyFunction', 'Function with no properties', schema); expect(result.parameters).toBeDefined(); expect(result.parameters!.type).toBe(Type.OBJECT); expect(result.parameters!.properties).toEqual({}); expect(result.parameters!.required).toEqual([]); }); it('should handle schema with no required fields', () => { const schema: ToolJsonSchema = { type: 'object', properties: { optional1: { type: 'string' }, optional2: { type: 'number' } } }; const result = toGeminiFunction('optionalFunction', 'Function with optional params', schema); expect(result.parameters).toBeDefined(); expect(result.parameters!.required).toEqual([]); expect(result.parameters!.properties).toBeDefined(); expect(result.parameters!.properties!['optional1']).toEqual({ type: Type.STRING }); expect(result.parameters!.properties!['optional2']).toEqual({ type: Type.NUMBER }); }); it('should default to object type when type is missing', () => { const schema: ToolJsonSchema = { properties: { field: { description: 'Field without type' } } }; const result = toGeminiFunction('defaultTypeFunction', 'Function with missing types', schema); expect(result.parameters).toBeDefined(); expect(result.parameters!.properties).toBeDefined(); const fieldProperty = result.parameters!.properties!['field']; expect(fieldProperty.type).toBe(Type.OBJECT); expect(fieldProperty.description).toBe('Field without type'); }); }); }); ================================================ FILE: src/extension/byok/common/test/geminiMessageConverter.spec.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Raw } from '@vscode/prompt-tsx'; import { describe, expect, it } from 'vitest'; import type { LanguageModelChatMessage } from 'vscode'; import { CustomDataPartMimeTypes } from '../../../../platform/endpoint/common/endpointTypes'; import { LanguageModelChatMessageRole, LanguageModelDataPart, LanguageModelTextPart, LanguageModelToolResultPart, LanguageModelTextPart as LMText } from '../../../../vscodeTypes'; import { apiMessageToGeminiMessage } from '../geminiMessageConverter'; describe('GeminiMessageConverter', () => { it('should convert basic user and assistant messages', () => { const messages: LanguageModelChatMessage[] = [ { role: LanguageModelChatMessageRole.User, content: [new LanguageModelTextPart('Hello, how are you?')], name: undefined }, { role: LanguageModelChatMessageRole.Assistant, content: [new LanguageModelTextPart('I am doing well, thank you!')], name: undefined } ]; const result = apiMessageToGeminiMessage(messages); expect(result.contents).toHaveLength(2); expect(result.contents[0].role).toBe('user'); expect(result.contents[0].parts).toBeDefined(); expect(result.contents[0].parts![0].text).toBe('Hello, how are you?'); expect(result.contents[1].role).toBe('model'); expect(result.contents[1].parts).toBeDefined(); expect(result.contents[1].parts![0].text).toBe('I am doing well, thank you!'); }); it('should handle system messages as system instruction', () => { const messages: LanguageModelChatMessage[] = [ { role: LanguageModelChatMessageRole.System, content: [new LanguageModelTextPart('You are a helpful assistant.')], name: undefined }, { role: LanguageModelChatMessageRole.User, content: [new LanguageModelTextPart('Hello!')], name: undefined } ]; const result = apiMessageToGeminiMessage(messages); expect(result.systemInstruction).toBeDefined(); expect(result.systemInstruction!.parts).toBeDefined(); expect(result.systemInstruction!.parts![0].text).toBe('You are a helpful assistant.'); expect(result.contents).toHaveLength(1); expect(result.contents[0].role).toBe('user'); }); it('should filter out empty text parts', () => { const messages: LanguageModelChatMessage[] = [ { role: LanguageModelChatMessageRole.User, content: [ new LanguageModelTextPart(''), new LanguageModelTextPart(' '), new LanguageModelTextPart('Hello!') ], name: undefined } ]; const result = apiMessageToGeminiMessage(messages); expect(result.contents[0].parts).toBeDefined(); expect(result.contents[0].parts!).toHaveLength(2); // Empty string filtered out, whitespace kept expect(result.contents[0].parts![0].text).toBe(' '); expect(result.contents[0].parts![1].text).toBe('Hello!'); }); it('should extract functionResponse parts from model message into subsequent user message and prune empty model', () => { // Simulate a model message that (incorrectly) contains only a tool result part const toolResult = new LanguageModelToolResultPart('myTool_12345', [new LanguageModelTextPart('{"foo":"bar"}')]); const messages: LanguageModelChatMessage[] = [ { role: LanguageModelChatMessageRole.Assistant, content: [toolResult], name: undefined } ]; const { contents } = apiMessageToGeminiMessage(messages); // The original (empty) model message should be pruned; we expect a single user message with functionResponse expect(contents).toHaveLength(1); expect(contents[0].role).toBe('user'); expect(contents[0].parts![0]).toHaveProperty('functionResponse'); const fr: any = contents[0].parts![0]; expect(fr.functionResponse.name).toBe('myTool'); // extracted from callId prefix expect(fr.functionResponse.response).toEqual({ foo: 'bar' }); }); it('should wrap array responses in an object', () => { const toolResult = new LanguageModelToolResultPart('listRepos_12345', [new LanguageModelTextPart('["repo1", "repo2"]')]); const messages: LanguageModelChatMessage[] = [ { role: LanguageModelChatMessageRole.Assistant, content: [toolResult], name: undefined } ]; const result = apiMessageToGeminiMessage(messages); expect(result.contents).toHaveLength(1); expect(result.contents[0].role).toBe('user'); const fr: any = result.contents[0].parts![0]; expect(fr.functionResponse.response).toEqual({ result: ['repo1', 'repo2'] }); }); it('should be idempotent when called multiple times (no duplication)', () => { const toolResult = new LanguageModelToolResultPart('doThing_12345', [new LMText('{"value":42}')]); const messages: LanguageModelChatMessage[] = [ { role: LanguageModelChatMessageRole.Assistant, content: [new LMText('Result:'), toolResult], name: undefined } ]; const first = apiMessageToGeminiMessage(messages); const second = apiMessageToGeminiMessage(messages); // Re-run with same original messages // Both runs should yield identical normalized structure (model text + user tool response) without growth expect(first.contents.length).toBe(2); expect(second.contents.length).toBe(2); expect(first.contents[0].role).toBe('model'); expect(first.contents[1].role).toBe('user'); expect(second.contents[0].role).toBe('model'); expect(second.contents[1].role).toBe('user'); }); describe('Image handling', () => { it('should handle LanguageModelDataPart as inline image data', () => { const imageData = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]); // PNG header const imagePart = new LanguageModelDataPart(imageData, 'image/png'); const messages: LanguageModelChatMessage[] = [ { role: LanguageModelChatMessageRole.User, content: [new LanguageModelTextPart('Here is an image:'), imagePart as any], name: undefined } ]; const result = apiMessageToGeminiMessage(messages); expect(result.contents).toHaveLength(1); expect(result.contents[0].parts).toHaveLength(2); expect(result.contents[0].parts![0].text).toBe('Here is an image:'); expect(result.contents[0].parts![1]).toHaveProperty('inlineData'); const inlineData: any = result.contents[0].parts![1]; expect(inlineData.inlineData.mimeType).toBe('image/png'); expect(inlineData.inlineData.data).toBe(Buffer.from(imageData).toString('base64')); }); it('should filter out StatefulMarker and CacheControl data parts', () => { const imageData = new Uint8Array([137, 80, 78, 71]); const validImage = new LanguageModelDataPart(imageData, 'image/jpeg'); const statefulMarker = new LanguageModelDataPart(new Uint8Array([1, 2, 3]), CustomDataPartMimeTypes.StatefulMarker); const cacheControl = new LanguageModelDataPart(new TextEncoder().encode('ephemeral'), CustomDataPartMimeTypes.CacheControl); const messages: LanguageModelChatMessage[] = [ { role: LanguageModelChatMessageRole.User, content: [validImage as any, statefulMarker as any, cacheControl as any], name: undefined } ]; const result = apiMessageToGeminiMessage(messages); // Should only include the valid image, not the stateful marker or cache control expect(result.contents[0].parts).toHaveLength(1); expect(result.contents[0].parts![0]).toHaveProperty('inlineData'); const inlineData: any = result.contents[0].parts![0]; expect(inlineData.inlineData.mimeType).toBe('image/jpeg'); }); it('should handle images in tool result content with text', () => { const imageData = new Uint8Array([255, 216, 255, 224]); // JPEG header const imagePart = new LanguageModelDataPart(imageData, 'image/jpeg'); const textPart = new LanguageModelTextPart('{"success": true}'); const toolResult = new LanguageModelToolResultPart('processImage_12345', [textPart, imagePart as any]); const messages: LanguageModelChatMessage[] = [ { role: LanguageModelChatMessageRole.Assistant, content: [toolResult], name: undefined } ]; const result = apiMessageToGeminiMessage(messages); // Should have a user message with function response expect(result.contents).toHaveLength(1); expect(result.contents[0].role).toBe('user'); expect(result.contents[0].parts![0]).toHaveProperty('functionResponse'); const fr: any = result.contents[0].parts![0]; expect(fr.functionResponse.name).toBe('processImage'); expect(fr.functionResponse.response.success).toBe(true); expect(fr.functionResponse.response.images).toBeDefined(); expect(fr.functionResponse.response.images).toHaveLength(1); expect(fr.functionResponse.response.images[0].mimeType).toBe('image/jpeg'); expect(fr.functionResponse.response.images[0].size).toBe(imageData.length); }); it('should handle images in tool result content without text', () => { const imageData1 = new Uint8Array([255, 216, 255, 224]); // JPEG header const imageData2 = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]); // PNG header const imagePart1 = new LanguageModelDataPart(imageData1, 'image/jpeg'); const imagePart2 = new LanguageModelDataPart(imageData2, 'image/png'); const toolResult = new LanguageModelToolResultPart('generateImages_12345', [imagePart1 as any, imagePart2 as any]); const messages: LanguageModelChatMessage[] = [ { role: LanguageModelChatMessageRole.Assistant, content: [toolResult], name: undefined } ]; const result = apiMessageToGeminiMessage(messages); expect(result.contents).toHaveLength(1); expect(result.contents[0].role).toBe('user'); const fr: any = result.contents[0].parts![0]; expect(fr.functionResponse.name).toBe('generateImages'); expect(fr.functionResponse.response.images).toHaveLength(2); // First image expect(fr.functionResponse.response.images[0].mimeType).toBe('image/jpeg'); expect(fr.functionResponse.response.images[0].size).toBe(imageData1.length); expect(fr.functionResponse.response.images[0].data).toBe(Buffer.from(imageData1).toString('base64')); // Second image expect(fr.functionResponse.response.images[1].mimeType).toBe('image/png'); expect(fr.functionResponse.response.images[1].size).toBe(imageData2.length); expect(fr.functionResponse.response.images[1].data).toBe(Buffer.from(imageData2).toString('base64')); }); it('should handle mixed text and filtered data parts in tool results', () => { const validImageData = new Uint8Array([255, 216]); const validImage = new LanguageModelDataPart(validImageData, 'image/jpeg'); const statefulMarker = new LanguageModelDataPart(new Uint8Array([1, 2, 3]), CustomDataPartMimeTypes.StatefulMarker); const textPart = new LanguageModelTextPart('Result text'); const toolResult = new LanguageModelToolResultPart('mixedContent_12345', [textPart, validImage as any, statefulMarker as any]); const messages: LanguageModelChatMessage[] = [ { role: LanguageModelChatMessageRole.Assistant, content: [toolResult], name: undefined } ]; const result = apiMessageToGeminiMessage(messages); const fr: any = result.contents[0].parts![0]; expect(fr.functionResponse.name).toBe('mixedContent'); // Should include text and valid image, but not stateful marker expect(fr.functionResponse.response.result).toContain('Result text'); expect(fr.functionResponse.response.result).toContain('[Contains 1 image(s) with types: image/jpeg]'); expect(fr.functionResponse.response.images).toHaveLength(1); expect(fr.functionResponse.response.images[0].mimeType).toBe('image/jpeg'); }); }); describe('geminiMessagesToRawMessages', () => { it('should convert function response with images to Raw format with image content parts', async () => { const { geminiMessagesToRawMessages } = await import('../geminiMessageConverter'); // Simulate a Gemini Content with function response containing images const contents = [{ role: 'user', parts: [{ functionResponse: { name: 'generateImages', response: { success: true, images: [ { mimeType: 'image/jpeg', size: 1024, data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==' }, { mimeType: 'image/png', size: 512, data: '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAYEBAQFBAYFBQYJBgUGCQsIBgYICwwKCgsKCgwQDAwMDAwMEAwODxAPDgwTExQUExMcGxsbHB8fHx8fHx8fHx//2wBDAQcHBw0MDRgQEBgaFREVGh8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx//wAARCAABAAEDAREAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=' } ] } } }] }]; const rawMessages = geminiMessagesToRawMessages(contents); expect(rawMessages).toHaveLength(1); // Check the role - should be Raw.ChatRole.Tool enum value expect(rawMessages[0].role).toBe(Raw.ChatRole.Tool); // Type assertion for tool message const toolMessage = rawMessages[0] as any; expect(toolMessage.toolCallId).toBe('generateImages'); expect(rawMessages[0].content).toHaveLength(3); // 2 images + 1 text part // Check first image expect(rawMessages[0].content[0].type).toBe(Raw.ChatCompletionContentPartKind.Image); const firstImage = rawMessages[0].content[0] as any; expect(firstImage.imageUrl?.url).toBe('data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='); // Check second image expect(rawMessages[0].content[1].type).toBe(Raw.ChatCompletionContentPartKind.Image); const secondImage = rawMessages[0].content[1] as any; expect(secondImage.imageUrl?.url).toBe('data:image/png;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAYEBAQFBAYFBQYJBgUGCQsIBgYICwwKCgsKCgwQDAwMDAwMEAwODxAPDgwTExQUExMcGxsbHB8fHx8fHx8fHx//2wBDAQcHBw0MDRgQEBgaFREVGh8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx//wAARCAABAAEDAREAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k='); // Check text content with cleaned response expect(rawMessages[0].content[2].type).toBe(Raw.ChatCompletionContentPartKind.Text); const textPart = rawMessages[0].content[2] as any; const textContent = JSON.parse(textPart.text); expect(textContent.success).toBe(true); expect(textContent.images).toHaveLength(2); expect(textContent.images[0].mimeType).toBe('image/jpeg'); expect(textContent.images[0].size).toBe(1024); expect(textContent.images[1].mimeType).toBe('image/png'); expect(textContent.images[1].size).toBe(512); // Should not contain raw base64 data in text content expect(textContent.images[0]).not.toHaveProperty('data'); expect(textContent.images[1]).not.toHaveProperty('data'); }); it('should handle function response without images normally', async () => { const { geminiMessagesToRawMessages } = await import('../geminiMessageConverter'); const contents = [{ role: 'user', parts: [{ functionResponse: { name: 'textFunction', response: { result: 'success', value: 42 } } }] }]; const rawMessages = geminiMessagesToRawMessages(contents); expect(rawMessages).toHaveLength(1); expect(rawMessages[0].role).toBe(Raw.ChatRole.Tool); expect(rawMessages[0].content).toHaveLength(1); expect(rawMessages[0].content[0].type).toBe(Raw.ChatCompletionContentPartKind.Text); const textPart = rawMessages[0].content[0] as any; expect(JSON.parse(textPart.text)).toEqual({ result: 'success', value: 42 }); }); }); }); ================================================ FILE: src/extension/byok/node/azureOpenAIEndpoint.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { OpenAIEndpoint } from './openAIEndpoint'; /** * Azure-specific OpenAI endpoint that supports Entra ID authentication. * Extends OpenAIEndpoint to override header generation for Azure-specific auth methods. * Note: Authentication token refresh is handled at the provider level (azureProvider.ts). */ export class AzureOpenAIEndpoint extends OpenAIEndpoint { /** * Override to use Entra ID authentication headers instead of API key. */ public override getExtraHeaders(): Record { const headers = super.getExtraHeaders(); headers['Authorization'] = `Bearer ${this._apiKey}`; // Defensive: Ensure 'api-key' header is never sent for Azure endpoints, even if parent class changes. delete headers['api-key']; return headers; } } ================================================ FILE: src/extension/byok/node/openAIEndpoint.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import type { CancellationToken } from 'vscode'; import { IChatMLFetcher } from '../../../platform/chat/common/chatMLFetcher'; import { ChatFetchResponseType, ChatResponse } from '../../../platform/chat/common/commonTypes'; import { IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { IDomainService } from '../../../platform/endpoint/common/domainService'; import { IChatModelInformation } from '../../../platform/endpoint/common/endpointProvider'; import { ChatEndpoint } from '../../../platform/endpoint/node/chatEndpoint'; import { ILogService } from '../../../platform/log/common/logService'; import { isOpenAiFunctionTool } from '../../../platform/networking/common/fetch'; import { createCapiRequestBody, IChatEndpoint, ICreateEndpointBodyOptions, IEndpointBody, IMakeChatRequestOptions } from '../../../platform/networking/common/networking'; import { RawMessageConversionCallback } from '../../../platform/networking/common/openai'; import { IChatWebSocketManager } from '../../../platform/networking/node/chatWebSocketManager'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { ITokenizerProvider } from '../../../platform/tokenizer/node/tokenizer'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; function hydrateBYOKErrorMessages(response: ChatResponse): ChatResponse { if (response.type === ChatFetchResponseType.Failed && response.streamError) { return { type: response.type, requestId: response.requestId, serverRequestId: response.serverRequestId, reason: JSON.stringify(response.streamError), }; } else if (response.type === ChatFetchResponseType.RateLimited) { return { type: response.type, requestId: response.requestId, serverRequestId: response.serverRequestId, reason: response.capiError ? 'Rate limit exceeded\n\n' + JSON.stringify(response.capiError) : 'Rate limit exceeded', rateLimitKey: '', retryAfter: undefined, isAuto: false, capiError: response.capiError }; } return response; } /** * Checks to see if a given endpoint is a BYOK model. * @param endpoint The endpoint to check if it's a BYOK model * @returns 1 if client side byok, 2 if server side byok, -1 if not a byok model */ export function isBYOKModel(endpoint: IChatEndpoint | undefined): number { if (!endpoint) { return -1; } return (endpoint instanceof OpenAIEndpoint || endpoint.isExtensionContributed) ? 1 : (endpoint.customModel ? 2 : -1); } export class OpenAIEndpoint extends ChatEndpoint { // Reserved headers that cannot be overridden for security and functionality reasons // Including forbidden request headers: https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_request_header private static readonly _reservedHeaders: ReadonlySet = new Set([ // Forbidden Request Headers 'accept-charset', 'accept-encoding', 'access-control-request-headers', 'access-control-request-method', 'connection', 'content-length', 'cookie', 'date', 'dnt', 'expect', 'host', 'keep-alive', 'origin', 'permissions-policy', 'referer', 'te', 'trailer', 'transfer-encoding', 'upgrade', 'user-agent', 'via', // Forwarding & Routing 'forwarded', 'x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', // Others 'api-key', 'authorization', 'content-type', 'openai-intent', 'x-github-api-version', 'x-initiator', 'x-interaction-id', 'x-interaction-type', 'x-onbehalf-extension-id', 'x-request-id', 'x-vscode-user-agent-library-version', // Pattern-based forbidden headers are checked separately: // - 'proxy-*' headers (handled in sanitization logic) // - 'sec-*' headers (handled in sanitization logic) // - 'x-http-method*' with forbidden methods CONNECT, TRACE, TRACK (handled in sanitization logic) ]); // RFC 7230 compliant header name pattern: token characters only private static readonly _validHeaderNamePattern = /^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$/; // Maximum limits to prevent abuse private static readonly _maxHeaderNameLength = 256; private static readonly _maxHeaderValueLength = 8192; private static readonly _maxCustomHeaderCount = 20; private readonly _customHeaders: Record; constructor( _modelMetadata: IChatModelInformation, protected readonly _apiKey: string, protected readonly _modelUrl: string, @IDomainService domainService: IDomainService, @IChatMLFetcher chatMLFetcher: IChatMLFetcher, @ITokenizerProvider tokenizerProvider: ITokenizerProvider, @IInstantiationService protected instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @IExperimentationService expService: IExperimentationService, @IChatWebSocketManager chatWebSocketService: IChatWebSocketManager, @ILogService protected logService: ILogService ) { super( _modelMetadata, domainService, chatMLFetcher, tokenizerProvider, instantiationService, configurationService, expService, chatWebSocketService, logService ); this._customHeaders = this._sanitizeCustomHeaders(_modelMetadata.requestHeaders); } private _sanitizeCustomHeaders(headers: Readonly> | undefined): Record { if (!headers) { return {}; } const entries = Object.entries(headers); if (entries.length > OpenAIEndpoint._maxCustomHeaderCount) { this.logService.warn(`[OpenAIEndpoint] Model '${this.modelMetadata.id}' has ${entries.length} custom headers, exceeding limit of ${OpenAIEndpoint._maxCustomHeaderCount}. Only first ${OpenAIEndpoint._maxCustomHeaderCount} will be processed.`); } const sanitized: Record = {}; let processedCount = 0; for (const [rawKey, rawValue] of entries) { if (processedCount >= OpenAIEndpoint._maxCustomHeaderCount) { break; } const key = rawKey.trim(); if (!key) { this.logService.warn(`[OpenAIEndpoint] Model '${this.modelMetadata.id}' has empty header name, skipping.`); continue; } if (key.length > OpenAIEndpoint._maxHeaderNameLength) { this.logService.warn(`[OpenAIEndpoint] Model '${this.modelMetadata.id}' has header name exceeding ${OpenAIEndpoint._maxHeaderNameLength} characters, skipping.`); continue; } if (!OpenAIEndpoint._validHeaderNamePattern.test(key)) { this.logService.warn(`[OpenAIEndpoint] Model '${this.modelMetadata.id}' has invalid header name format: '${key}', Skipping.`); continue; } const lowerKey = key.toLowerCase(); if (OpenAIEndpoint._reservedHeaders.has(lowerKey)) { this.logService.warn(`[OpenAIEndpoint] Model '${this.modelMetadata.id}' attempted to override reserved header '${key}', skipping.`); continue; } // Check for pattern-based forbidden headers if (lowerKey.startsWith('proxy-') || lowerKey.startsWith('sec-')) { this.logService.warn(`[OpenAIEndpoint] Model '${this.modelMetadata.id}' attempted to set forbidden header pattern '${key}', skipping.`); continue; } // Check for X-HTTP-Method* headers with forbidden methods if ((lowerKey === 'x-http-method' || lowerKey === 'x-http-method-override' || lowerKey === 'x-method-override')) { const forbiddenMethods = ['connect', 'trace', 'track']; const methodValue = String(rawValue).toLowerCase().trim(); if (forbiddenMethods.includes(methodValue)) { this.logService.warn(`[OpenAIEndpoint] Model '${this.modelMetadata.id}' attempted to set forbidden method '${methodValue}' in header '${key}', skipping.`); continue; } } const sanitizedValue = this._sanitizeHeaderValue(rawValue); if (sanitizedValue === undefined) { this.logService.warn(`[OpenAIEndpoint] Model '${this.modelMetadata.id}' has invalid value for header '${key}': '${rawValue}', skipping.`); continue; } sanitized[key] = sanitizedValue; processedCount++; } return sanitized; } private _sanitizeHeaderValue(value: unknown): string | undefined { if (typeof value !== 'string') { return undefined; } const trimmed = value.trim(); if (trimmed.length > OpenAIEndpoint._maxHeaderValueLength) { return undefined; } // Disallow control characters including CR, LF, and others (0x00-0x1F, 0x7F) // This prevents HTTP header injection and response splitting attacks if (/[\x00-\x1F\x7F]/.test(trimmed)) { return undefined; } // Additional check for potential Unicode issues // Reject headers with bidirectional override characters or zero-width characters if (/[\u200B-\u200D\u202A-\u202E\uFEFF]/.test(trimmed)) { return undefined; } return trimmed; } override createRequestBody(options: ICreateEndpointBodyOptions): IEndpointBody { if (this.useResponsesApi) { // Handle Responses API: customize the body directly options.ignoreStatefulMarker = false; const body = super.createRequestBody(options); body.store = true; body.n = undefined; body.stream_options = undefined; if (!this.modelMetadata.capabilities.supports.thinking) { body.reasoning = undefined; body.include = undefined; } if (body.previous_response_id && (!body.previous_response_id.startsWith('resp_') || this.modelMetadata.zeroDataRetentionEnabled)) { // Don't use a response ID from CAPI or when zero data retention is enabled body.previous_response_id = undefined; } return body; } else { // Handle CAPI: provide callback for thinking data processing const callback: RawMessageConversionCallback = (out, data) => { if (data && data.id) { out.cot_id = data.id; out.cot_summary = Array.isArray(data.text) ? data.text.join('') : data.text; } }; const body = createCapiRequestBody(options, this.model, callback); return body; } } override interceptBody(body: IEndpointBody | undefined): void { super.interceptBody(body); // TODO @lramos15 - We should do this for all models and not just here if (body?.tools?.length === 0) { delete body.tools; } if (body?.tools) { body.tools = body.tools.map(tool => { if (isOpenAiFunctionTool(tool) && tool.function.parameters === undefined) { tool.function.parameters = { type: 'object', properties: {} }; } return tool; }); } if (body) { if (this.modelMetadata.capabilities.supports.thinking) { delete body.temperature; body['max_completion_tokens'] = body.max_tokens; delete body.max_tokens; } // Removing max tokens defaults to the maximum which is what we want for BYOK delete body.max_tokens; if (!this.useResponsesApi && body.stream) { body['stream_options'] = { 'include_usage': true }; } } } override get urlOrRequestMetadata(): string { return this._modelUrl; } public override getExtraHeaders(): Record { const headers: Record = { 'Content-Type': 'application/json' }; if (this._modelUrl.includes('openai.azure')) { headers['api-key'] = this._apiKey; } else { headers['Authorization'] = `Bearer ${this._apiKey}`; } for (const [key, value] of Object.entries(this._customHeaders)) { headers[key] = value; } return headers; } override cloneWithTokenOverride(modelMaxPromptTokens: number): IChatEndpoint { const newModelInfo = { ...this.modelMetadata, maxInputTokens: modelMaxPromptTokens }; return this.instantiationService.createInstance(OpenAIEndpoint, newModelInfo, this._apiKey, this._modelUrl); } public override async makeChatRequest2(options: IMakeChatRequestOptions, token: CancellationToken): Promise { // Apply ignoreStatefulMarker: false for initial request const modifiedOptions: IMakeChatRequestOptions = { ...options, ignoreStatefulMarker: false }; const response = await super.makeChatRequest2(modifiedOptions, token); return hydrateBYOKErrorMessages(response); } } ================================================ FILE: src/extension/byok/node/test/azureOpenAIEndpoint.spec.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { IChatModelInformation, ModelSupportedEndpoint } from '../../../../platform/endpoint/common/endpointProvider'; import { ITestingServicesAccessor } from '../../../../platform/test/node/services'; import { TokenizerType } from '../../../../util/common/tokenizer'; import { DisposableStore } from '../../../../util/vs/base/common/lifecycle'; import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation'; import { createExtensionUnitTestingServices } from '../../../test/node/services'; import { AzureOpenAIEndpoint } from '../azureOpenAIEndpoint'; describe('AzureOpenAIEndpoint', () => { let modelMetadata: IChatModelInformation; const disposables = new DisposableStore(); let accessor: ITestingServicesAccessor; let instaService: IInstantiationService; beforeEach(() => { modelMetadata = { id: 'test-azure-model', vendor: 'Microsoft Azure', name: 'Test Azure Model', version: '1.0', model_picker_enabled: true, is_chat_default: false, is_chat_fallback: false, supported_endpoints: [ModelSupportedEndpoint.ChatCompletions], capabilities: { type: 'chat', family: 'openai', tokenizer: TokenizerType.O200K, supports: { parallel_tool_calls: false, streaming: true, tool_calls: false, vision: false, prediction: false, thinking: false }, limits: { max_prompt_tokens: 128000, max_output_tokens: 4096, max_context_window_tokens: 132096 } } }; const testingServiceCollection = createExtensionUnitTestingServices(); accessor = disposables.add(testingServiceCollection.createTestingAccessor()); instaService = accessor.get(IInstantiationService); }); afterEach(() => { disposables.clear(); }); describe('getExtraHeaders', () => { it('should use Authorization header with Bearer token for Entra ID authentication', () => { const entraToken = 'test-entra-token-abc123'; const endpoint = instaService.createInstance( AzureOpenAIEndpoint, modelMetadata, entraToken, 'https://example-endpoint.example.com/v1/chat/completions' ); const headers = endpoint.getExtraHeaders(); // Should have Authorization header with Bearer token expect(headers['Authorization']).toBe(`Bearer ${entraToken}`); // Should NOT have api-key header (Azure API key auth) expect(headers['api-key']).toBeUndefined(); // Should have standard headers expect(headers['Content-Type']).toBe('application/json'); }); it('should override parent class headers to replace api-key with Authorization', () => { const entraToken = 'test-entra-token-xyz789'; const endpoint = instaService.createInstance( AzureOpenAIEndpoint, modelMetadata, entraToken, 'https://example-endpoint.example.com/v1/chat/completions' ); const headers = endpoint.getExtraHeaders(); // Verify the override worked correctly expect(headers['Authorization']).toBe(`Bearer ${entraToken}`); expect(headers['api-key']).toBeUndefined(); expect(Object.keys(headers)).not.toContain('api-key'); }); it('should work with different Azure OpenAI endpoint URLs', () => { const entraToken = 'test-token-456'; // Test with different endpoint formats const urls = [ 'https://example-endpoint-1.example.com/v1/chat/completions', 'https://example-endpoint-2.example.com/v1/chat/completions', 'https://example-endpoint-3.example.com/v1/chat/completions' ]; for (const url of urls) { const endpoint = instaService.createInstance( AzureOpenAIEndpoint, modelMetadata, entraToken, url ); const headers = endpoint.getExtraHeaders(); expect(headers['Authorization']).toBe(`Bearer ${entraToken}`); expect(headers['api-key']).toBeUndefined(); } }); it('should preserve other headers from parent class', () => { const entraToken = 'test-token-789'; const endpoint = instaService.createInstance( AzureOpenAIEndpoint, modelMetadata, entraToken, 'https://example-endpoint.example.com/v1/chat/completions' ); const headers = endpoint.getExtraHeaders(); // Should preserve Content-Type from parent expect(headers['Content-Type']).toBe('application/json'); // Should have Authorization header expect(headers['Authorization']).toBeDefined(); expect(headers['Authorization']).toContain('Bearer'); }); }); describe('inheritance', () => { it('should inherit from OpenAIEndpoint and maintain same constructor signature', () => { const entraToken = 'test-token-inheritance'; // Should be able to instantiate with same parameters as OpenAIEndpoint const endpoint = instaService.createInstance( AzureOpenAIEndpoint, modelMetadata, entraToken, 'https://example-endpoint.example.com/v1/chat/completions' ); // Should be an instance of AzureOpenAIEndpoint expect(endpoint).toBeInstanceOf(AzureOpenAIEndpoint); // Should have getExtraHeaders method expect(typeof endpoint.getExtraHeaders).toBe('function'); }); }); }); ================================================ FILE: src/extension/byok/node/test/openAIEndpoint.spec.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Raw } from '@vscode/prompt-tsx'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService'; import { IChatModelInformation, ModelSupportedEndpoint } from '../../../../platform/endpoint/common/endpointProvider'; import { ICreateEndpointBodyOptions } from '../../../../platform/networking/common/networking'; import { ITestingServicesAccessor } from '../../../../platform/test/node/services'; import { DisposableStore } from '../../../../util/vs/base/common/lifecycle'; import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation'; import { createExtensionUnitTestingServices } from '../../../test/node/services'; import { OpenAIEndpoint } from '../openAIEndpoint'; // Test fixtures for thinking content const createThinkingMessage = (thinkingId: string, thinkingText: string): Raw.ChatMessage => ({ role: Raw.ChatRole.Assistant, content: [ { type: Raw.ChatCompletionContentPartKind.Opaque, value: { type: 'thinking', thinking: { id: thinkingId, text: thinkingText } } } ] }); const createTestOptions = (messages: Raw.ChatMessage[]): ICreateEndpointBodyOptions => ({ debugName: 'test', messages, requestId: 'test-req-123', postOptions: {}, finishedCb: undefined, location: undefined as any }); describe('OpenAIEndpoint - Reasoning Properties', () => { let modelMetadata: IChatModelInformation; const disposables = new DisposableStore(); let accessor: ITestingServicesAccessor; let instaService: IInstantiationService; beforeEach(() => { modelMetadata = { id: 'test-model', name: 'Test Model', vendor: 'Test Vendor', version: '1.0', model_picker_enabled: true, is_chat_default: false, is_chat_fallback: false, supported_endpoints: [ModelSupportedEndpoint.ChatCompletions, ModelSupportedEndpoint.Responses], capabilities: { type: 'chat', family: 'openai', tokenizer: 'o200k_base' as any, supports: { parallel_tool_calls: false, streaming: true, tool_calls: false, vision: false, prediction: false, thinking: true }, limits: { max_prompt_tokens: 4096, max_output_tokens: 2048, max_context_window_tokens: 6144 } } }; const testingServiceCollection = createExtensionUnitTestingServices(); accessor = disposables.add(testingServiceCollection.createTestingAccessor()); instaService = accessor.get(IInstantiationService); }); afterEach(() => { disposables.clear(); }); describe('CAPI mode (useResponsesApi = false)', () => { it('should set cot_id and cot_summary properties when processing thinking content', () => { const endpoint = instaService.createInstance(OpenAIEndpoint, { ...modelMetadata, supported_endpoints: [ModelSupportedEndpoint.ChatCompletions] }, 'test-api-key', 'https://api.openai.com/v1/chat/completions'); const thinkingMessage = createThinkingMessage('test-thinking-123', 'this is my reasoning'); const options = createTestOptions([thinkingMessage]); const body = endpoint.createRequestBody(options); expect(body.messages).toBeDefined(); const messages = body.messages as any[]; expect(messages).toHaveLength(1); expect(messages[0].cot_id).toBe('test-thinking-123'); expect(messages[0].cot_summary).toBe('this is my reasoning'); }); it('should handle multiple messages with thinking content', () => { const endpoint = instaService.createInstance(OpenAIEndpoint, { ...modelMetadata, supported_endpoints: [ModelSupportedEndpoint.ChatCompletions] }, 'test-api-key', 'https://api.openai.com/v1/chat/completions'); const userMessage: Raw.ChatMessage = { role: Raw.ChatRole.User, content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Hello' }] }; const thinkingMessage = createThinkingMessage('reasoning-456', 'complex reasoning here'); const options = createTestOptions([userMessage, thinkingMessage]); const body = endpoint.createRequestBody(options); expect(body.messages).toBeDefined(); const messages = body.messages as any[]; expect(messages).toHaveLength(2); // User message should not have thinking properties expect(messages[0].cot_id).toBeUndefined(); expect(messages[0].cot_summary).toBeUndefined(); // Assistant message should have thinking properties expect(messages[1].cot_id).toBe('reasoning-456'); expect(messages[1].cot_summary).toBe('complex reasoning here'); }); }); describe('Responses API mode (useResponsesApi = true)', () => { it('should preserve reasoning object when thinking is supported', () => { accessor.get(IConfigurationService).setConfig(ConfigKey.ResponsesApiReasoningSummary, 'detailed'); const endpoint = instaService.createInstance(OpenAIEndpoint, modelMetadata, 'test-api-key', 'https://api.openai.com/v1/chat/completions'); const thinkingMessage = createThinkingMessage('resp-api-789', 'responses api reasoning'); const options = createTestOptions([thinkingMessage]); const body = endpoint.createRequestBody(options); expect(body.store).toBe(true); expect(body.n).toBeUndefined(); expect(body.stream_options).toBeUndefined(); expect(body.reasoning).toBeDefined(); // Should preserve reasoning object }); it('should remove reasoning object when thinking is not supported', () => { const modelWithoutThinking = { ...modelMetadata, capabilities: { ...modelMetadata.capabilities, supports: { ...modelMetadata.capabilities.supports, thinking: false } } }; accessor.get(IConfigurationService).setConfig(ConfigKey.ResponsesApiReasoningSummary, 'detailed'); const endpoint = instaService.createInstance(OpenAIEndpoint, modelWithoutThinking, 'test-api-key', 'https://api.openai.com/v1/chat/completions'); const thinkingMessage = createThinkingMessage('no-thinking-999', 'should be removed'); const options = createTestOptions([thinkingMessage]); const body = endpoint.createRequestBody(options); expect(body.reasoning).toBeUndefined(); // Should be removed }); }); }); ================================================ FILE: src/extension/byok/vscode-node/abstractLanguageModelChatProvider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { CancellationToken, commands, LanguageModelChatInformation, LanguageModelChatMessage, LanguageModelChatMessage2, LanguageModelChatProvider, LanguageModelResponsePart2, PrepareLanguageModelChatModelOptions, Progress, ProvideLanguageModelChatResponseOptions } from 'vscode'; import { IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { IChatModelInformation, ModelSupportedEndpoint } from '../../../platform/endpoint/common/endpointProvider'; import { ILogService } from '../../../platform/log/common/logService'; import { IFetcherService } from '../../../platform/networking/common/fetcherService'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { IStringDictionary } from '../../../util/vs/base/common/collections'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { CopilotLanguageModelWrapper } from '../../conversation/vscode-node/languageModelAccess'; import { BYOKAuthType, BYOKKnownModels, byokKnownModelsToAPIInfo, BYOKModelCapabilities, resolveModelInfo } from '../common/byokProvider'; import { OpenAIEndpoint } from '../node/openAIEndpoint'; import { IBYOKStorageService } from './byokStorageService'; export interface LanguageModelChatConfiguration { readonly apiKey?: string; } export interface ExtendedLanguageModelChatInformation extends LanguageModelChatInformation { readonly configuration?: C; } export abstract class AbstractLanguageModelChatProvider = ExtendedLanguageModelChatInformation> implements LanguageModelChatProvider { constructor( protected readonly _id: string, protected readonly _name: string, protected _knownModels: BYOKKnownModels | undefined, protected readonly _byokStorageService: IBYOKStorageService, @ILogService protected readonly _logService: ILogService, ) { this.configureDefaultGroupWithApiKeyOnly(); } // TODO: Remove this after 6 months protected async configureDefaultGroupWithApiKeyOnly(): Promise { const apiKey = await this._byokStorageService.getAPIKey(this._name); if (apiKey) { this.configureDefaultGroupIfExists(this._name, { apiKey } as C); await this._byokStorageService.deleteAPIKey(this._name, BYOKAuthType.GlobalApiKey); } return apiKey; } protected async configureDefaultGroupIfExists(name: string, configuration: C): Promise { await commands.executeCommand('lm.migrateLanguageModelsProviderGroup', { vendor: this._id, name, ...configuration }); } async provideLanguageModelChatInformation({ silent, configuration }: PrepareLanguageModelChatModelOptions, token: CancellationToken): Promise { let apiKey: string | undefined = (configuration as C)?.apiKey; if (!apiKey) { apiKey = await this.configureDefaultGroupWithApiKeyOnly(); } const models = await this.getAllModels(silent, apiKey, configuration as C); return models.map(model => ({ ...model, apiKey, configuration })); } abstract provideLanguageModelChatResponse(model: T, messages: Array, options: ProvideLanguageModelChatResponseOptions, progress: Progress, token: CancellationToken): Promise; abstract provideTokenCount(model: T, text: string | LanguageModelChatMessage | LanguageModelChatMessage2, token: CancellationToken): Promise; protected abstract getAllModels(silent: boolean, apiKey: string | undefined, configuration: C | undefined): Promise; } export interface OpenAICompatibleLanguageModelChatInformation extends ExtendedLanguageModelChatInformation { url: string; } export abstract class AbstractOpenAICompatibleLMProvider extends AbstractLanguageModelChatProvider> { protected readonly _lmWrapper: CopilotLanguageModelWrapper; constructor( id: string, name: string, knownModels: BYOKKnownModels | undefined, byokStorageService: IBYOKStorageService, @IFetcherService protected readonly _fetcherService: IFetcherService, logService: ILogService, @IInstantiationService protected readonly _instantiationService: IInstantiationService, @IConfigurationService protected readonly _configurationService: IConfigurationService, @IExperimentationService protected readonly _expService: IExperimentationService ) { super(id, name, knownModels, byokStorageService, logService); this._lmWrapper = this._instantiationService.createInstance(CopilotLanguageModelWrapper); } async provideLanguageModelChatResponse(model: OpenAICompatibleLanguageModelChatInformation, messages: Array, options: ProvideLanguageModelChatResponseOptions, progress: Progress, token: CancellationToken): Promise { const openAIChatEndpoint = await this.createOpenAIEndPoint(model); return this._lmWrapper.provideLanguageModelResponse(openAIChatEndpoint, messages, options, options.requestInitiator, progress, token); } async provideTokenCount(model: OpenAICompatibleLanguageModelChatInformation, text: string | LanguageModelChatMessage | LanguageModelChatMessage2, token: CancellationToken): Promise { const openAIChatEndpoint = await this.createOpenAIEndPoint(model); return this._lmWrapper.provideTokenCount(openAIChatEndpoint, text); } protected async getAllModels(silent: boolean, apiKey: string | undefined, configuration: T | undefined): Promise[]> { const modelsUrl = this.getModelsBaseUrl(configuration); if (modelsUrl) { const models = await this.getModelsFromEndpoint(modelsUrl, silent, apiKey); return byokKnownModelsToAPIInfo(this._name, models).map(model => ({ ...model, url: modelsUrl })); } return []; } private async getModelsFromEndpoint(endpoint: string, silent: boolean, apiKey: string | undefined): Promise { if (!apiKey && silent) { return {}; } try { const headers: IStringDictionary = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }; const modelsEndpoint = this.getModelsDiscoveryUrl(endpoint); const response = await this._fetcherService.fetch(modelsEndpoint, { method: 'GET', headers, callSite: 'byok-models-discovery', }); const data = await response.json(); const modelList: BYOKKnownModels = {}; const models = data.data ?? data.models; if (!models || !Array.isArray(models)) { throw new Error('Invalid response format'); } for (const model of models) { let modelCapabilities = this._knownModels?.[model.id]; if (!modelCapabilities) { modelCapabilities = this.resolveModelCapabilities(model); if (!modelCapabilities) { continue; } if (!this._knownModels) { this._knownModels = {}; } this._knownModels[model.id] = modelCapabilities; } modelList[model.id] = modelCapabilities; } return modelList; } catch (error) { this._logService.error(error, `Error fetching available OpenRouter models`); throw error; } } protected async createOpenAIEndPoint(model: OpenAICompatibleLanguageModelChatInformation): Promise { const modelInfo = this.getModelInfo(model.id, model.url); const url = modelInfo.supported_endpoints?.includes(ModelSupportedEndpoint.Responses) ? `${model.url}/responses` : `${model.url}/chat/completions`; return this._instantiationService.createInstance(OpenAIEndpoint, modelInfo, model.configuration?.apiKey ?? '', url); } protected getModelInfo(modelId: string, modelUrl: string): IChatModelInformation { return resolveModelInfo(modelId, this._name, this._knownModels); } protected resolveModelCapabilities(modelData: unknown): BYOKModelCapabilities | undefined { return undefined; } protected abstract getModelsBaseUrl(configuration: T | undefined): string | undefined; protected getModelsDiscoveryUrl(modelsBaseUrl: string): string { return `${modelsBaseUrl}/models`; } } ================================================ FILE: src/extension/byok/vscode-node/anthropicProvider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import Anthropic from '@anthropic-ai/sdk'; import * as vscode from 'vscode'; import { CancellationToken, LanguageModelChatInformation, LanguageModelChatMessage, LanguageModelChatMessage2, LanguageModelDataPart, LanguageModelResponsePart2, LanguageModelTextPart, LanguageModelThinkingPart, LanguageModelToolCallPart, LanguageModelToolResultPart, Progress, ProvideLanguageModelChatResponseOptions } from 'vscode'; import { ChatFetchResponseType, ChatLocation } from '../../../platform/chat/common/commonTypes'; import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { CustomDataPartMimeTypes } from '../../../platform/endpoint/common/endpointTypes'; import { ILogService } from '../../../platform/log/common/logService'; import { ContextManagementResponse, getContextManagementFromConfig, isAnthropicContextEditingEnabled, isAnthropicMemoryToolEnabled, isAnthropicToolSearchEnabled, nonDeferredToolNames, TOOL_SEARCH_TOOL_NAME, TOOL_SEARCH_TOOL_TYPE, ToolSearchToolResult, ToolSearchToolSearchResult } from '../../../platform/networking/common/anthropic'; import { IResponseDelta, OpenAiFunctionTool } from '../../../platform/networking/common/fetch'; import { APIUsage } from '../../../platform/networking/common/openai'; import { CopilotChatAttr, emitInferenceDetailsEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, type OTelModelOptions, StdAttr, truncateForOTel } from '../../../platform/otel/common/index'; import { IOTelService, SpanKind, SpanStatusCode } from '../../../platform/otel/common/otelService'; import { IRequestLogger, retrieveCapturingTokenByCorrelation, runWithCapturingToken } from '../../../platform/requestLogger/node/requestLogger'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; import { toErrorMessage } from '../../../util/common/errorMessage'; import { RecordedProgress } from '../../../util/common/progressRecorder'; import { generateUuid } from '../../../util/vs/base/common/uuid'; import { anthropicMessagesToRawMessagesForLogging, apiMessageToAnthropicMessage } from '../common/anthropicMessageConverter'; import { BYOKKnownModels, byokKnownModelsToAPIInfo, BYOKModelCapabilities, LMResponsePart } from '../common/byokProvider'; import { AbstractLanguageModelChatProvider, ExtendedLanguageModelChatInformation, LanguageModelChatConfiguration } from './abstractLanguageModelChatProvider'; import { IBYOKStorageService } from './byokStorageService'; export class AnthropicLMProvider extends AbstractLanguageModelChatProvider { public static readonly providerName = 'Anthropic'; constructor( knownModels: BYOKKnownModels | undefined, byokStorageService: IBYOKStorageService, @ILogService logService: ILogService, @IRequestLogger private readonly _requestLogger: IRequestLogger, @IConfigurationService private readonly _configurationService: IConfigurationService, @IExperimentationService private readonly _experimentationService: IExperimentationService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IOTelService private readonly _otelService: IOTelService, ) { super(AnthropicLMProvider.providerName.toLowerCase(), AnthropicLMProvider.providerName, knownModels, byokStorageService, logService); } private _getThinkingBudget(modelId: string, maxOutputTokens: number): number | undefined { const configuredBudget = this._configurationService.getExperimentBasedConfig(ConfigKey.AnthropicThinkingBudget, this._experimentationService); if (!configuredBudget || configuredBudget === 0) { return undefined; } const modelCapabilities = this._knownModels?.[modelId]; const modelSupportsThinking = modelCapabilities?.thinking ?? false; if (!modelSupportsThinking) { return undefined; } const normalizedBudget = configuredBudget < 1024 ? 1024 : configuredBudget; return Math.min(32000, maxOutputTokens - 1, normalizedBudget); } // Filters the byok known models based on what the anthropic API knows as well protected async getAllModels(silent: boolean, apiKey: string | undefined): Promise[]> { if (!apiKey && silent) { return []; } try { const response = await new Anthropic({ apiKey }).models.list(); const modelList: Record = {}; for (const model of response.data) { if (this._knownModels && this._knownModels[model.id]) { modelList[model.id] = this._knownModels[model.id]; } else { // Mix in generic capabilities for models we don't know modelList[model.id] = { maxInputTokens: 100000, maxOutputTokens: 16000, name: model.display_name, toolCalling: true, vision: false, thinking: false }; } } return byokKnownModelsToAPIInfo(this._name, modelList); } catch (error) { this._logService.error(error, `Error fetching available ${AnthropicLMProvider.providerName} models`); throw new Error(error.message ? error.message : error); } } async provideLanguageModelChatResponse(model: ExtendedLanguageModelChatInformation, messages: Array, options: ProvideLanguageModelChatResponseOptions, progress: Progress, token: CancellationToken): Promise { // Restore CapturingToken context if correlation ID was passed through modelOptions. // This handles the case where AsyncLocalStorage context was lost crossing VS Code IPC. const correlationId = (options as { modelOptions?: OTelModelOptions }).modelOptions?._capturingTokenCorrelationId; const capturingToken = correlationId ? retrieveCapturingTokenByCorrelation(correlationId) : undefined; // Restore OTel trace context to link spans back to the agent trace const parentTraceContext = (options as { modelOptions?: OTelModelOptions }).modelOptions?._otelTraceContext ?? undefined; // OTel span handle — created outside doRequest, enriched inside with usage data let otelSpan: ReturnType | undefined; const doRequest = async () => { const issuedTime = Date.now(); const apiKey = model.configuration?.apiKey; if (!apiKey) { throw new Error('API key not found for the model'); } const anthropicClient = new Anthropic({ apiKey }); // Convert the messages from the API format into messages that we can use against anthropic const { system, messages: convertedMessages } = apiMessageToAnthropicMessage(messages as LanguageModelChatMessage[]); const requestId = generateUuid(); const pendingLoggedChatRequest = this._requestLogger.logChatRequest( 'AnthropicBYOK', { model: model.id, modelMaxPromptTokens: model.maxInputTokens, urlOrRequestMetadata: anthropicClient.baseURL, }, { model: model.id, messages: anthropicMessagesToRawMessagesForLogging(convertedMessages, system), ourRequestId: requestId, location: ChatLocation.Other, body: { tools: options.tools?.map((tool): OpenAiFunctionTool => ({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.inputSchema } })) }, }); const memoryToolEnabled = isAnthropicMemoryToolEnabled(model.id, this._configurationService, this._experimentationService); const toolSearchEnabled = isAnthropicToolSearchEnabled(model.id.replace(/-/g, '.'), this._configurationService); // Build tools array, handling both standard tools and native Anthropic tools const tools: Anthropic.Beta.BetaToolUnion[] = []; // Add tool search tool if enabled (must be first in the array) if (toolSearchEnabled) { tools.push({ name: TOOL_SEARCH_TOOL_NAME, type: TOOL_SEARCH_TOOL_TYPE, defer_loading: false } as Anthropic.Beta.BetaToolUnion); } let hasMemoryTool = false; for (const tool of (options.tools ?? [])) { // Handle native Anthropic memory tool (only for models that support it) if (tool.name === 'memory' && memoryToolEnabled) { hasMemoryTool = true; tools.push({ name: 'memory', type: 'memory_20250818' } as Anthropic.Beta.BetaMemoryTool20250818); continue; } // Mark tools for deferred loading when tool search is enabled, except for frequently used tools const shouldDefer = toolSearchEnabled ? !nonDeferredToolNames.has(tool.name) : undefined; if (!tool.inputSchema) { tools.push({ name: tool.name, description: tool.description, input_schema: { type: 'object', properties: {}, required: [] }, ...(shouldDefer ? { defer_loading: shouldDefer } : {}) }); continue; } tools.push({ name: tool.name, description: tool.description, input_schema: { type: 'object', properties: (tool.inputSchema as { properties?: Record }).properties ?? {}, required: (tool.inputSchema as { required?: string[] }).required ?? [], $schema: (tool.inputSchema as { $schema?: unknown }).$schema }, ...(shouldDefer ? { defer_loading: shouldDefer } : {}) }); } // Check if web search is enabled and append web_search tool if not already present. // We need to do this because there is no local web_search tool definition we can replace. const webSearchEnabled = this._configurationService.getExperimentBasedConfig(ConfigKey.AnthropicWebSearchToolEnabled, this._experimentationService); if (webSearchEnabled && !tools.some(tool => 'name' in tool && tool.name === 'web_search')) { const maxUses = this._configurationService.getConfig(ConfigKey.AnthropicWebSearchMaxUses); const allowedDomains = this._configurationService.getConfig(ConfigKey.AnthropicWebSearchAllowedDomains); const blockedDomains = this._configurationService.getConfig(ConfigKey.AnthropicWebSearchBlockedDomains); const userLocation = this._configurationService.getConfig(ConfigKey.AnthropicWebSearchUserLocation); const shouldDeferWebSearch = toolSearchEnabled ? !nonDeferredToolNames.has('web_search') : undefined; const webSearchTool: Anthropic.Beta.BetaWebSearchTool20250305 = { name: 'web_search', type: 'web_search_20250305', max_uses: maxUses, ...(shouldDeferWebSearch ? { defer_loading: shouldDeferWebSearch } : {}) }; // Add domain filtering if configured // Cannot use both allowed and blocked domains simultaneously if (allowedDomains && allowedDomains.length > 0) { webSearchTool.allowed_domains = allowedDomains; } else if (blockedDomains && blockedDomains.length > 0) { webSearchTool.blocked_domains = blockedDomains; } // Add user location if configured // Note: All fields are optional according to Anthropic docs if (userLocation && (userLocation.city || userLocation.region || userLocation.country || userLocation.timezone)) { webSearchTool.user_location = { type: 'approximate', ...userLocation }; } tools.push(webSearchTool); } const thinkingBudget = this._getThinkingBudget(model.id, model.maxOutputTokens); // Check if model supports adaptive thinking const modelCapabilities = this._knownModels?.[model.id]; const forceNonAdaptive = this._configurationService.getExperimentBasedConfig(ConfigKey.AnthropicForceExtendedThinking, this._experimentationService); const supportsAdaptiveThinking = (modelCapabilities?.adaptiveThinking ?? false) && !forceNonAdaptive; // Build context management configuration const thinkingEnabled = supportsAdaptiveThinking || (thinkingBudget ?? 0) > 0; const contextManagement = isAnthropicContextEditingEnabled(model.id, this._configurationService, this._experimentationService) ? getContextManagementFromConfig( this._configurationService, this._experimentationService, thinkingEnabled ) : undefined; // Build betas array for beta API features (adaptive thinking doesn't need interleaved-thinking beta) const betas: string[] = []; if (thinkingBudget && !supportsAdaptiveThinking) { betas.push('interleaved-thinking-2025-05-14'); } else if (forceNonAdaptive && (modelCapabilities?.adaptiveThinking ?? false)) { betas.push('interleaved-thinking-2025-05-14'); } if (hasMemoryTool || contextManagement) { betas.push('context-management-2025-06-27'); } if (toolSearchEnabled) { betas.push('advanced-tool-use-2025-11-20'); } const rawEffort = options.modelConfiguration?.reasoningEffort; const effort = supportsAdaptiveThinking && typeof rawEffort === 'string' ? rawEffort as 'low' | 'medium' | 'high' : undefined; const params: Anthropic.Beta.Messages.MessageCreateParamsStreaming = { model: model.id, messages: convertedMessages, max_tokens: model.maxOutputTokens, stream: true, system: [system], tools: tools.length > 0 ? tools : undefined, thinking: supportsAdaptiveThinking ? { type: 'adaptive' as const } : thinkingBudget ? { type: 'enabled' as const, budget_tokens: thinkingBudget } : undefined, ...(effort ? { output_config: { effort } } : {}), context_management: contextManagement as Anthropic.Beta.Messages.BetaContextManagementConfig | undefined, }; const wrappedProgress = new RecordedProgress(progress); try { const result = await this._makeRequest(anthropicClient, wrappedProgress, params, betas, token, issuedTime); if (result.ttft) { pendingLoggedChatRequest.markTimeToFirstToken(result.ttft); } const responseDeltas: IResponseDelta[] = wrappedProgress.items.map((i): IResponseDelta => { if (i instanceof LanguageModelTextPart) { return { text: i.value }; } else if (i instanceof LanguageModelToolCallPart) { return { text: '', copilotToolCalls: [{ name: i.name, arguments: JSON.stringify(i.input), id: i.callId }] }; } else if (i instanceof LanguageModelToolResultPart) { // Handle tool results - extract text from content const resultText = i.content.map(c => c instanceof LanguageModelTextPart ? c.value : '').join(''); return { text: `[Tool Result ${i.callId}]: ${resultText}` }; } else { return { text: '' }; } }); // TODO: @bhavyaus - Add telemetry tracking for context editing (contextEditingApplied, contextEditingClearedTokens, contextEditingEditCount) like messagesApi.ts does if (result.contextManagement) { responseDeltas.push({ text: '', contextManagement: result.contextManagement }); } pendingLoggedChatRequest.resolve({ type: ChatFetchResponseType.Success, requestId, serverRequestId: requestId, usage: result.usage, value: ['value'], resolvedModel: model.id }, responseDeltas); // Enrich OTel span with usage data from the Anthropic response if (otelSpan && result.usage) { otelSpan.setAttributes({ [GenAiAttr.USAGE_INPUT_TOKENS]: result.usage.prompt_tokens ?? 0, [GenAiAttr.USAGE_OUTPUT_TOKENS]: result.usage.completion_tokens ?? 0, ...(result.usage.prompt_tokens_details?.cached_tokens ? { [GenAiAttr.USAGE_CACHE_READ_INPUT_TOKENS]: result.usage.prompt_tokens_details.cached_tokens } : {}), [GenAiAttr.RESPONSE_MODEL]: model.id, [GenAiAttr.RESPONSE_ID]: requestId, [GenAiAttr.RESPONSE_FINISH_REASONS]: ['stop'], [GenAiAttr.CONVERSATION_ID]: requestId, ...(result.ttft ? { [CopilotChatAttr.TIME_TO_FIRST_TOKEN]: result.ttft } : {}), [GenAiAttr.REQUEST_MAX_TOKENS]: model.maxOutputTokens ?? 0, }); // Opt-in content capture if (this._otelService.config.captureContent) { const responseText = wrappedProgress.items .filter((p): p is LanguageModelTextPart => p instanceof LanguageModelTextPart) .map(p => p.value).join(''); const toolCalls = wrappedProgress.items .filter((p): p is LanguageModelToolCallPart => p instanceof LanguageModelToolCallPart) .map(tc => ({ type: 'tool_call' as const, id: tc.callId, name: tc.name, arguments: tc.input })); const parts: Array<{ type: string; content?: string; id?: string; name?: string; arguments?: unknown }> = []; if (responseText) { parts.push({ type: 'text', content: responseText }); } parts.push(...toolCalls); if (parts.length > 0) { otelSpan.setAttribute(GenAiAttr.OUTPUT_MESSAGES, truncateForOTel(JSON.stringify([{ role: 'assistant', parts }]))); } } } // Record OTel metrics for this Anthropic LLM call if (result.usage) { const durationSec = (Date.now() - issuedTime) / 1000; const metricAttrs = { operationName: GenAiOperationName.CHAT, providerName: 'anthropic', requestModel: model.id, responseModel: model.id }; GenAiMetrics.recordOperationDuration(this._otelService, durationSec, metricAttrs); if (result.usage.prompt_tokens) { GenAiMetrics.recordTokenUsage(this._otelService, result.usage.prompt_tokens, 'input', metricAttrs); } if (result.usage.completion_tokens) { GenAiMetrics.recordTokenUsage(this._otelService, result.usage.completion_tokens, 'output', metricAttrs); } if (result.ttft) { GenAiMetrics.recordTimeToFirstToken(this._otelService, model.id, result.ttft / 1000); } } // Emit OTel inference details event emitInferenceDetailsEvent( this._otelService, { model: model.id, maxTokens: model.maxOutputTokens }, result.usage ? { id: requestId, model: model.id, finishReasons: ['stop'], inputTokens: result.usage.prompt_tokens, outputTokens: result.usage.completion_tokens, } : undefined, ); // Send success telemetry matching response.success format /* __GDPR__ "response.success" : { "owner": "digitarald", "comment": "Report quality details for a successful service response.", "reason": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Reason for why a response finished" }, "filterReason": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Reason for why a response was filtered" }, "source": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Source of the initial request" }, "initiatorType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the request was initiated by a user or an agent" }, "model": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Model selection for the response" }, "modelInvoked": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Actual model invoked for the response" }, "apiType": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "API type for the response- chat completions or responses" }, "requestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Id of the current turn request" }, "gitHubRequestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "GitHub request id if available" }, "associatedRequestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Another request ID that this request is associated with (eg, the originating request of a summarization request)." }, "reasoningEffort": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Reasoning effort level" }, "reasoningSummary": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Reasoning summary level" }, "fetcher": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The fetcher used for the request" }, "transport": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The transport used for the request (http or websocket)" }, "totalTokenMax": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Maximum total token window", "isMeasurement": true }, "clientPromptTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of prompt tokens, locally counted", "isMeasurement": true }, "promptTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of prompt tokens, server side counted", "isMeasurement": true }, "promptCacheTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of prompt tokens hitting cache as reported by server", "isMeasurement": true }, "tokenCountMax": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Maximum generated tokens", "isMeasurement": true }, "tokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of generated tokens", "isMeasurement": true }, "reasoningTokens": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of reasoning tokens", "isMeasurement": true }, "acceptedPredictionTokens": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of tokens in the prediction that appeared in the completion", "isMeasurement": true }, "rejectedPredictionTokens": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of tokens in the prediction that appeared in the completion", "isMeasurement": true }, "completionTokens": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of tokens in the output", "isMeasurement": true }, "timeToFirstToken": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Time to first token", "isMeasurement": true }, "timeToFirstTokenEmitted": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Time to first token emitted (visible text)", "isMeasurement": true }, "timeToComplete": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Time to complete the request", "isMeasurement": true }, "issuedTime": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Timestamp when the request was issued", "isMeasurement": true }, "isVisionRequest": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether the request was for a vision model", "isMeasurement": true }, "isBYOK": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the request was for a BYOK model", "isMeasurement": true }, "isAuto": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the request was for an Auto model", "isMeasurement": true }, "bytesReceived": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of bytes received in the response", "isMeasurement": true }, "retryAfterError": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Error of the original request." }, "retryAfterErrorGitHubRequestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "GitHub request id of the original request if available" }, "connectivityTestError": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Error of the connectivity test." }, "connectivityTestErrorGitHubRequestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "GitHub request id of the connectivity test request if available" }, "retryAfterFilterCategory": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "If the response was filtered and this is a retry attempt, this contains the original filtered content category." }, "suspendEventSeen": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether a system suspend event was seen during the request", "isMeasurement": true }, "resumeEventSeen": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether a system resume event was seen during the request", "isMeasurement": true } } */ this._telemetryService.sendTelemetryEvent('response.success', { github: true, microsoft: true }, { source: 'byok.anthropic', model: model.id, requestId, }, { totalTokenMax: model.maxInputTokens ?? -1, tokenCountMax: model.maxOutputTokens ?? -1, promptTokenCount: result.usage?.prompt_tokens, promptCacheTokenCount: result.usage?.prompt_tokens_details?.cached_tokens, tokenCount: result.usage?.total_tokens, completionTokens: result.usage?.completion_tokens, timeToFirstToken: result.ttft, timeToFirstTokenEmitted: result.ttfte, timeToComplete: Date.now() - issuedTime, issuedTime, isBYOK: 1, }); } catch (err) { this._logService.error(`BYOK Anthropic error: ${toErrorMessage(err, true)}`); pendingLoggedChatRequest.resolve({ type: ChatFetchResponseType.Unknown, requestId, serverRequestId: requestId, reason: err.message }, wrappedProgress.items.map((i): IResponseDelta => { if (i instanceof LanguageModelTextPart) { return { text: i.value }; } else if (i instanceof LanguageModelToolCallPart) { return { text: '', copilotToolCalls: [{ name: i.name, arguments: JSON.stringify(i.input), id: i.callId }] }; } else if (i instanceof LanguageModelToolResultPart) { // Handle tool results - extract text from content const resultText = i.content.map(c => c instanceof LanguageModelTextPart ? c.value : '').join(''); return { text: `[Tool Result ${i.callId}]: ${resultText}` }; } else { return { text: '' }; } })); throw err; } }; // Create OTel span and execute with trace context + CapturingToken const executeRequest = async () => { otelSpan = this._otelService.startSpan(`chat ${model.id}`, { kind: SpanKind.CLIENT, attributes: { [GenAiAttr.OPERATION_NAME]: GenAiOperationName.CHAT, [GenAiAttr.PROVIDER_NAME]: 'anthropic', [GenAiAttr.REQUEST_MODEL]: model.id, [GenAiAttr.AGENT_NAME]: 'AnthropicBYOK', [CopilotChatAttr.MAX_PROMPT_TOKENS]: model.maxInputTokens, [StdAttr.SERVER_ADDRESS]: 'api.anthropic.com', }, }); // Opt-in: capture input messages if (this._otelService.config.captureContent) { try { const roleNames: Record = { 1: 'user', 2: 'assistant', 3: 'system' }; const inputMsgs = messages.map(m => { const msg = m as LanguageModelChatMessage; const role = roleNames[msg.role] ?? String(msg.role); const textParts: string[] = []; if (Array.isArray(msg.content)) { for (const p of msg.content) { if (p instanceof LanguageModelTextPart) { textParts.push(p.value); } } } const content = textParts.length > 0 ? textParts.join('') : '[non-text content]'; return { role, parts: [{ type: 'text', content }] }; }); otelSpan.setAttribute(GenAiAttr.INPUT_MESSAGES, truncateForOTel(JSON.stringify(inputMsgs))); } catch { /* swallow */ } } try { const result = capturingToken ? await runWithCapturingToken(capturingToken, doRequest) : await doRequest(); otelSpan.setStatus(SpanStatusCode.OK); return result; } catch (err) { otelSpan.setStatus(SpanStatusCode.ERROR, err instanceof Error ? err.message : String(err)); throw err; } finally { otelSpan.end(); } }; if (parentTraceContext) { return this._otelService.runWithTraceContext(parentTraceContext, executeRequest); } return executeRequest(); } async provideTokenCount(model: LanguageModelChatInformation, text: string | LanguageModelChatMessage | LanguageModelChatMessage2, token: CancellationToken): Promise { // Simple estimation - actual token count would require Claude's tokenizer return Math.ceil(text.toString().length / 4); } private async _makeRequest(anthropicClient: Anthropic, progress: RecordedProgress, params: Anthropic.Beta.Messages.MessageCreateParamsStreaming, betas: string[], token: CancellationToken, issuedTime: number): Promise<{ ttft: number | undefined; ttfte: number | undefined; usage: APIUsage | undefined; contextManagement: ContextManagementResponse | undefined }> { const start = Date.now(); let ttft: number | undefined; let ttfte: number | undefined; const stream = await anthropicClient.beta.messages.create({ ...params, ...(betas.length > 0 && { betas }) }); let pendingToolCall: { toolId?: string; name?: string; jsonInput?: string; } | undefined; let pendingThinking: { thinking?: string; signature?: string; } | undefined; let pendingRedactedThinking: { data: string; } | undefined; let pendingServerToolCall: { toolId?: string; name?: string; jsonInput?: string; type?: string; } | undefined; let usage: APIUsage | undefined; let contextManagementResponse: ContextManagementResponse | undefined; let hasText = false; for await (const chunk of stream) { if (token.isCancellationRequested) { break; } if (ttft === undefined) { ttft = Date.now() - start; } this._logService.trace(`chunk: ${JSON.stringify(chunk)}`); if (chunk.type === 'content_block_start') { if ('content_block' in chunk && chunk.content_block.type === 'tool_use') { pendingToolCall = { toolId: chunk.content_block.id, name: chunk.content_block.name, jsonInput: '' }; } else if ('content_block' in chunk && chunk.content_block.type === 'server_tool_use') { // Handle server-side tool use (e.g., web_search) pendingServerToolCall = { toolId: chunk.content_block.id, name: chunk.content_block.name, jsonInput: '', type: chunk.content_block.name }; progress.report(new LanguageModelTextPart('\n')); } else if ('content_block' in chunk && chunk.content_block.type === 'thinking') { pendingThinking = { thinking: '', signature: '' }; } else if ('content_block' in chunk && chunk.content_block.type === 'redacted_thinking') { const redactedBlock = chunk.content_block as Anthropic.Messages.RedactedThinkingBlock; pendingRedactedThinking = { data: redactedBlock.data }; } else if ('content_block' in chunk && chunk.content_block.type === 'web_search_tool_result') { if (!pendingServerToolCall || !pendingServerToolCall.toolId) { continue; } const resultBlock = chunk.content_block as Anthropic.Messages.WebSearchToolResultBlock; // Handle potential error in web search if (!Array.isArray(resultBlock.content)) { this._logService.error(`Web search error: ${(resultBlock.content as Anthropic.Messages.WebSearchToolResultError).error_code}`); continue; } const results = resultBlock.content.map((result: Anthropic.Messages.WebSearchResultBlock) => ({ type: 'web_search_result', url: result.url, title: result.title, page_age: result.page_age, encrypted_content: result.encrypted_content })); // Format according to Anthropic's web_search_tool_result specification const toolResult = { type: 'web_search_tool_result', tool_use_id: pendingServerToolCall.toolId, content: results }; const searchResults = JSON.stringify(toolResult, null, 2); // TODO: @bhavyaus - instead of just pushing text, create a specialized WebSearchResult part progress.report(new LanguageModelToolResultPart( pendingServerToolCall.toolId!, [new LanguageModelTextPart(searchResults)] )); pendingServerToolCall = undefined; } else if ('content_block' in chunk && chunk.content_block.type === 'tool_search_tool_result') { const toolSearchResult = chunk.content_block as unknown as ToolSearchToolResult; if (toolSearchResult.content.type === 'tool_search_tool_search_result') { const searchResult = toolSearchResult.content as ToolSearchToolSearchResult; const toolNames = searchResult.tool_references.map(ref => ref.tool_name); this._logService.trace(`Tool search discovered ${toolNames.length} tools: ${toolNames.join(', ')}`); let query: string | undefined; if (pendingServerToolCall) { try { const parsed = JSON.parse(pendingServerToolCall.jsonInput || '{}'); query = parsed.query; } catch { // Ignore parse errors } } progress.report(new LanguageModelToolResultPart( toolSearchResult.tool_use_id, [new LanguageModelTextPart(JSON.stringify({ query, discovered_tools: toolNames }))] )); pendingServerToolCall = undefined; } else if (toolSearchResult.content.type === 'tool_search_tool_result_error') { this._logService.warn(`Tool search error: ${toolSearchResult.content.error_code}`); pendingServerToolCall = undefined; } } continue; } if (chunk.type === 'content_block_delta') { if (chunk.delta.type === 'text_delta') { progress.report(new LanguageModelTextPart(chunk.delta.text || '')); if (!hasText && chunk.delta.text?.length > 0) { ttfte = Date.now() - issuedTime; } hasText ||= chunk.delta.text?.length > 0; } else if (chunk.delta.type === 'citations_delta') { if ('citation' in chunk.delta) { // TODO: @bhavyaus - instead of just pushing text, create a specialized Citation part const citation = chunk.delta.citation as Anthropic.Messages.CitationsWebSearchResultLocation; if (citation.type === 'web_search_result_location') { // Format citation according to Anthropic specification const citationData = { type: 'web_search_result_location', url: citation.url, title: citation.title, encrypted_index: citation.encrypted_index, cited_text: citation.cited_text }; // Format citation as readable blockquote with source link const referenceText = `\n> "${citation.cited_text}" — [${vscode.l10n.t('Source')}](${citation.url})\n\n`; // Report formatted reference text to user progress.report(new LanguageModelTextPart(referenceText)); // Store the citation data in the correct format for multi-turn conversations progress.report(new LanguageModelToolResultPart( 'citation', [new LanguageModelTextPart(JSON.stringify(citationData, null, 2))] )); } } } else if (chunk.delta.type === 'thinking_delta') { if (pendingThinking) { pendingThinking.thinking = (pendingThinking.thinking || '') + (chunk.delta.thinking || ''); progress.report(new LanguageModelThinkingPart(chunk.delta.thinking || '')); } } else if (chunk.delta.type === 'signature_delta') { // Accumulate signature if (pendingThinking) { pendingThinking.signature = (pendingThinking.signature || '') + (chunk.delta.signature || ''); } } else if (chunk.delta.type === 'input_json_delta' && pendingToolCall) { pendingToolCall.jsonInput = (pendingToolCall.jsonInput || '') + (chunk.delta.partial_json || ''); try { // Try to parse the accumulated JSON to see if it's complete const parsedJson = JSON.parse(pendingToolCall.jsonInput); progress.report(new LanguageModelToolCallPart( pendingToolCall.toolId!, pendingToolCall.name!, parsedJson )); pendingToolCall = undefined; } catch { // JSON is not complete yet, continue accumulating continue; } } else if (chunk.delta.type === 'input_json_delta' && pendingServerToolCall) { pendingServerToolCall.jsonInput = (pendingServerToolCall.jsonInput || '') + (chunk.delta.partial_json || ''); } } if (chunk.type === 'content_block_stop') { if (pendingToolCall) { try { const parsedJson = JSON.parse(pendingToolCall.jsonInput || '{}'); progress.report( new LanguageModelToolCallPart( pendingToolCall.toolId!, pendingToolCall.name!, parsedJson ) ); } catch (e) { console.error('Failed to parse tool call JSON:', e); } pendingToolCall = undefined; } else if (pendingThinking) { if (pendingThinking.signature) { const finalThinkingPart = new LanguageModelThinkingPart(''); finalThinkingPart.metadata = { signature: pendingThinking.signature, _completeThinking: pendingThinking.thinking }; progress.report(finalThinkingPart); } pendingThinking = undefined; } else if (pendingRedactedThinking) { pendingRedactedThinking = undefined; } } if (chunk.type === 'message_start') { // TODO final output tokens: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":46}} usage = { completion_tokens: -1, prompt_tokens: chunk.message.usage.input_tokens + (chunk.message.usage.cache_creation_input_tokens ?? 0) + (chunk.message.usage.cache_read_input_tokens ?? 0), total_tokens: -1, // Cast needed: Anthropic returns cache_creation_input_tokens which APIUsage.prompt_tokens_details doesn't define prompt_tokens_details: { cached_tokens: chunk.message.usage.cache_read_input_tokens ?? 0, cache_creation_input_tokens: chunk.message.usage.cache_creation_input_tokens } as any }; } else if (usage && chunk.type === 'message_delta') { if (chunk.usage.output_tokens) { usage.completion_tokens = chunk.usage.output_tokens; usage.total_tokens = usage.prompt_tokens + chunk.usage.output_tokens; } // Handle context management response if ('context_management' in chunk && chunk.context_management) { contextManagementResponse = chunk.context_management as ContextManagementResponse; const totalClearedTokens = contextManagementResponse.applied_edits.reduce( (sum, edit) => sum + (edit.cleared_input_tokens || 0), 0 ); this._logService.info(`BYOK Anthropic context editing applied: cleared ${totalClearedTokens} tokens across ${contextManagementResponse.applied_edits.length} edits`); // Emit context management via LanguageModelDataPart so it flows through to toolCallingLoop progress.report(new LanguageModelDataPart( new TextEncoder().encode(JSON.stringify(contextManagementResponse)), CustomDataPartMimeTypes.ContextManagement )); } } } return { ttft, ttfte, usage, contextManagement: contextManagementResponse }; } } ================================================ FILE: src/extension/byok/vscode-node/azureProvider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import { CancellationToken, LanguageModelChatMessage, LanguageModelChatMessage2, LanguageModelResponsePart2, Progress, ProvideLanguageModelChatResponseOptions } from 'vscode'; import { AzureAuthMode, ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { isEndpointEditToolName } from '../../../platform/endpoint/common/endpointProvider'; import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; import { ILogService } from '../../../platform/log/common/logService'; import { IFetcherService } from '../../../platform/networking/common/fetcherService'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { resolveModelInfo } from '../common/byokProvider'; import { AzureOpenAIEndpoint } from '../node/azureOpenAIEndpoint'; import { OpenAICompatibleLanguageModelChatInformation } from './abstractLanguageModelChatProvider'; import { IBYOKStorageService } from './byokStorageService'; import { AbstractCustomOAIBYOKModelProvider, CustomOAIModelProviderConfig, hasExplicitApiPath } from './customOAIProvider'; export function resolveAzureUrl(modelId: string, url: string): string { // The fully resolved url was already passed in if (hasExplicitApiPath(url)) { return url; } // Remove the trailing slash if (url.endsWith('/')) { url = url.slice(0, -1); } // if url ends with `/v1` remove it if (url.endsWith('/v1')) { url = url.slice(0, -3); } // Default to chat completions for base URLs const defaultApiPath = '/chat/completions'; if (url.includes('models.ai.azure.com') || url.includes('inference.ml.azure.com')) { return `${url}/v1${defaultApiPath}`; } else if (url.includes('openai.azure.com')) { return `${url}/openai/deployments/${modelId}${defaultApiPath}?api-version=2025-01-01-preview`; } else { throw new Error(`Unrecognized Azure deployment URL: ${url}`); } } export class AzureBYOKModelProvider extends AbstractCustomOAIBYOKModelProvider { static readonly providerName = 'Azure'; constructor( byokStorageService: IBYOKStorageService, @IConfigurationService configurationService: IConfigurationService, @ILogService logService: ILogService, @IFetcherService fetcherService: IFetcherService, @IInstantiationService instantiationService: IInstantiationService, @IExperimentationService expService: IExperimentationService, @IVSCodeExtensionContext extensionContext: IVSCodeExtensionContext ) { super( AzureBYOKModelProvider.providerName.toLowerCase(), AzureBYOKModelProvider.providerName, byokStorageService, logService, fetcherService, instantiationService, configurationService, expService, extensionContext ); this.migrateExistingConfigs(); } // TODO: Remove this after 6 months private async migrateExistingConfigs(): Promise { await this.migrateConfig(ConfigKey.Deprecated.AzureModels, AzureBYOKModelProvider.providerName, AzureBYOKModelProvider.providerName); await this._configurationService.setConfig(ConfigKey.Deprecated.AzureAuthType, undefined); } protected override resolveUrl(modelId: string, url: string): string { return resolveAzureUrl(modelId, url); } override async provideLanguageModelChatResponse( model: OpenAICompatibleLanguageModelChatInformation, messages: Array, options: ProvideLanguageModelChatResponseOptions, progress: Progress, token: CancellationToken ): Promise { if (model.configuration?.apiKey) { return super.provideLanguageModelChatResponse(model, messages, options, progress, token); } const session: vscode.AuthenticationSession = await vscode.authentication.getSession( AzureAuthMode.MICROSOFT_AUTH_PROVIDER, [AzureAuthMode.COGNITIVE_SERVICES_SCOPE], { createIfNone: true, silent: false } ); const url = this.resolveUrl(model.id, model.url); const modelConfiguration = model.configuration?.models?.find(m => m.id === model.id); const modelCapabilities = { maxInputTokens: model.maxInputTokens, maxOutputTokens: model.maxOutputTokens, toolCalling: !!model.capabilities?.toolCalling || false, vision: !!model.capabilities?.imageInput || false, name: model.name, url, thinking: modelConfiguration?.thinking, streaming: modelConfiguration?.streaming, requestHeaders: modelConfiguration?.requestHeaders, editTools: model.capabilities?.editTools?.filter(isEndpointEditToolName), zeroDataRetentionEnabled: modelConfiguration?.zeroDataRetentionEnabled }; const modelInfo = resolveModelInfo(model.id, this._name, undefined, modelCapabilities); const openAIChatEndpoint = this._instantiationService.createInstance( AzureOpenAIEndpoint, modelInfo, session.accessToken, // Pass Entra ID token url ); return this._lmWrapper.provideLanguageModelResponse( openAIChatEndpoint, messages, options, options.requestInitiator, progress, token ); } } ================================================ FILE: src/extension/byok/vscode-node/byokContribution.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { LanguageModelChatInformation, LanguageModelChatProvider, lm } from 'vscode'; import { IAuthenticationService } from '../../../platform/authentication/common/authentication'; import { ICAPIClientService } from '../../../platform/endpoint/common/capiClient'; import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; import { ILogService } from '../../../platform/log/common/logService'; import { IFetcherService } from '../../../platform/networking/common/fetcherService'; import { Disposable } from '../../../util/vs/base/common/lifecycle'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { BYOKKnownModels, isBYOKEnabled } from '../../byok/common/byokProvider'; import { IExtensionContribution } from '../../common/contributions'; import { AnthropicLMProvider } from './anthropicProvider'; import { AzureBYOKModelProvider } from './azureProvider'; import { BYOKStorageService, IBYOKStorageService } from './byokStorageService'; import { CustomOAIBYOKModelProvider } from './customOAIProvider'; import { GeminiNativeBYOKLMProvider } from './geminiNativeProvider'; import { OllamaLMProvider } from './ollamaProvider'; import { OAIBYOKLMProvider } from './openAIProvider'; import { OpenRouterLMProvider } from './openRouterProvider'; import { XAIBYOKLMProvider } from './xAIProvider'; export class BYOKContrib extends Disposable implements IExtensionContribution { public readonly id: string = 'byok-contribution'; private readonly _byokStorageService: IBYOKStorageService; private readonly _providers: Map> = new Map(); private _byokProvidersRegistered = false; constructor( @IFetcherService private readonly _fetcherService: IFetcherService, @ILogService private readonly _logService: ILogService, @ICAPIClientService private readonly _capiClientService: ICAPIClientService, @IVSCodeExtensionContext extensionContext: IVSCodeExtensionContext, @IAuthenticationService authService: IAuthenticationService, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); this._byokStorageService = new BYOKStorageService(extensionContext); this._authChange(authService, this._instantiationService); this._register(authService.onDidAuthenticationChange(() => { this._authChange(authService, this._instantiationService); })); } private async _authChange(authService: IAuthenticationService, instantiationService: IInstantiationService) { if (authService.copilotToken && isBYOKEnabled(authService.copilotToken, this._capiClientService) && !this._byokProvidersRegistered) { this._byokProvidersRegistered = true; // Update known models list from CDN so all providers have the same list const knownModels = await this.fetchKnownModelList(this._fetcherService); if (this._store.isDisposed) { return; } this._providers.set(OllamaLMProvider.providerName.toLowerCase(), instantiationService.createInstance(OllamaLMProvider, this._byokStorageService)); this._providers.set(AnthropicLMProvider.providerName.toLowerCase(), instantiationService.createInstance(AnthropicLMProvider, knownModels[AnthropicLMProvider.providerName], this._byokStorageService)); this._providers.set(GeminiNativeBYOKLMProvider.providerName.toLowerCase(), instantiationService.createInstance(GeminiNativeBYOKLMProvider, knownModels[GeminiNativeBYOKLMProvider.providerName], this._byokStorageService)); this._providers.set(XAIBYOKLMProvider.providerName.toLowerCase(), instantiationService.createInstance(XAIBYOKLMProvider, knownModels[XAIBYOKLMProvider.providerName], this._byokStorageService)); this._providers.set(OAIBYOKLMProvider.providerName.toLowerCase(), instantiationService.createInstance(OAIBYOKLMProvider, knownModels[OAIBYOKLMProvider.providerName], this._byokStorageService)); this._providers.set(OpenRouterLMProvider.providerName.toLowerCase(), instantiationService.createInstance(OpenRouterLMProvider, this._byokStorageService)); this._providers.set(AzureBYOKModelProvider.providerName.toLowerCase(), instantiationService.createInstance(AzureBYOKModelProvider, this._byokStorageService)); this._providers.set(CustomOAIBYOKModelProvider.providerName.toLowerCase(), instantiationService.createInstance(CustomOAIBYOKModelProvider, this._byokStorageService)); for (const [providerName, provider] of this._providers) { this._store.add(lm.registerLanguageModelChatProvider(providerName, provider)); } } } private async fetchKnownModelList(fetcherService: IFetcherService): Promise> { const data = await (await fetcherService.fetch('https://main.vscode-cdn.net/extensions/copilotChat.json', { method: 'GET', callSite: 'byok-known-models' })).json(); // Use this for testing with changes from a local file. Don't check in // const data = JSON.parse((await this._fileSystemService.readFile(URI.file('/Users/roblou/code/vscode-engineering/chat/copilotChat.json'))).toString()); let knownModels: Record; if (data.version !== 1) { this._logService.warn('BYOK: Copilot Chat known models list is not in the expected format. Defaulting to empty list.'); knownModels = {}; } else { knownModels = data.modelInfo; } this._logService.info('BYOK: Copilot Chat known models list fetched successfully.'); return knownModels; } } ================================================ FILE: src/extension/byok/vscode-node/byokStorageService.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; import { BYOKAuthType, BYOKModelCapabilities } from '../../byok/common/byokProvider'; export interface StoredModelConfig { deploymentUrl?: string; isRegistered?: boolean; // Will be undefined for now but eventually storage will update to be true / false. isCustomModel?: boolean; // Will be undefined for now but eventually storage will update to be true / false. modelCapabilities?: BYOKModelCapabilities; } export interface IBYOKStorageService { /** * Get API key for a provider or model */ getAPIKey(providerName: string, modelId?: string): Promise; /** * Store API key for a provider or model based on auth type */ storeAPIKey(providerName: string, apiKey: string, authType: BYOKAuthType, modelId?: string): Promise; /** * Delete API key for a provider or model based on auth type */ deleteAPIKey(providerName: string, authType: BYOKAuthType, modelId?: string): Promise; /** * Get all stored model configurations for a provider */ getStoredModelConfigs(providerName: string): Promise>; /** * Save model configuration to storage */ saveModelConfig( modelId: string, providerName: string, config: { apiKey: string; deploymentUrl?: string; modelCapabilities?: BYOKModelCapabilities; }, authType: BYOKAuthType ): Promise; /** * Handles the cases * 1. Non custom model, and isDeletingCustomModel = false -> Delete from storage as we have the known model list * 2. Custom model, and isDeletingCustomModel = true -> Delete from storage as we have the known model list * 3. Custom model, and isDeletingCustomModel = false -> Do not delete from storage as we do not have the known model list. Instead mark unregistered */ removeModelConfig(modelId: string, providerName: string, isDeletingCustomModel: boolean): Promise; } export class BYOKStorageService implements IBYOKStorageService { private readonly _extensionContext: IVSCodeExtensionContext; constructor(extensionContext: IVSCodeExtensionContext) { this._extensionContext = extensionContext; } public async getAPIKey(providerName: string, modelId?: string): Promise { // If model-specific key is requested, try to get it first if (modelId) { const modelKey = await this._extensionContext.secrets.get(`copilot-byok-${providerName}-${modelId}-api-key`); // Only return the key if it's non-empty after trimming, and return the trimmed version if (modelKey && modelKey.trim()) { return modelKey.trim(); } } // Fall back to provider key if no model-specific key or it was requested directly const providerKey = await this._extensionContext.secrets.get(`copilot-byok-${providerName}-api-key`); // Only return the key if it's non-empty after trimming, and return the trimmed version return providerKey?.trim() || undefined; } public async storeAPIKey(providerName: string, apiKey: string, authType: BYOKAuthType, modelId?: string): Promise { // Store API keys based on the provider's auth type if (authType === BYOKAuthType.None) { // Don't store keys for None auth type providers return; } // Ignore empty or whitespace-only API keys. // This prevents invalid keys from being stored if (!apiKey?.trim()) { return; } if (authType === BYOKAuthType.GlobalApiKey) { // For GlobalApiKey providers, only store at provider level await this._extensionContext.secrets.store(`copilot-byok-${providerName}-api-key`, apiKey); } else if (authType === BYOKAuthType.PerModelDeployment && modelId) { // For PerModelDeployment providers, store per model await this._extensionContext.secrets.store(`copilot-byok-${providerName}-${modelId}-api-key`, apiKey); } } public async deleteAPIKey(providerName: string, authType: BYOKAuthType, modelId?: string): Promise { // Delete API keys based on the provider's auth type if (authType === BYOKAuthType.None) { // Nothing to delete for None auth type providers return; } else if (authType === BYOKAuthType.GlobalApiKey) { // For GlobalApiKey providers, delete at provider level await this._extensionContext.secrets.delete(`copilot-byok-${providerName}-api-key`); } else if (authType === BYOKAuthType.PerModelDeployment && modelId) { // For PerModelDeployment providers, delete per model await this._extensionContext.secrets.delete(`copilot-byok-${providerName}-${modelId}-api-key`); } } public async getStoredModelConfigs(providerName: string): Promise> { return this._extensionContext.globalState.get>( `copilot-byok-${providerName}-models-config`, {} ); } public async saveModelConfig( modelId: string, providerName: string, config: { apiKey: string; isCustomModel: boolean; deploymentUrl?: string; modelCapabilities?: BYOKModelCapabilities; }, authType: BYOKAuthType ): Promise { // Save model configuration data const configToSave: StoredModelConfig = { isCustomModel: config.isCustomModel, deploymentUrl: config.deploymentUrl, isRegistered: true, modelCapabilities: config.modelCapabilities }; const existingConfigs = await this.getStoredModelConfigs(providerName); existingConfigs[modelId] = configToSave; await this._extensionContext.globalState.update(`copilot-byok-${providerName}-models-config`, existingConfigs); await this.storeAPIKey(providerName, config.apiKey, authType, modelId); } public async removeModelConfig(modelId: string, providerName: string, isDeletingCustomModel: boolean): Promise { const existingConfigs = await this.getStoredModelConfigs(providerName); const existingConfig = existingConfigs[modelId]; const isCustomModel = existingConfig?.isCustomModel || false; if (existingConfig && (isDeletingCustomModel || !isCustomModel)) { delete existingConfigs[modelId]; await this._extensionContext.globalState.update( `copilot-byok-${providerName}-models-config`, existingConfigs ); // Remove API key from secrets await this._extensionContext.secrets.delete(`copilot-byok-${providerName}-${modelId}-api-key`); } else { existingConfig.isRegistered = false; await this._extensionContext.globalState.update( `copilot-byok-${providerName}-models-config`, existingConfigs ); } } } ================================================ FILE: src/extension/byok/vscode-node/customOAIProvider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Config, ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { EndpointEditToolName, ModelSupportedEndpoint } from '../../../platform/endpoint/common/endpointProvider'; import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; import { ILogService } from '../../../platform/log/common/logService'; import { IFetcherService } from '../../../platform/networking/common/fetcherService'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { IStringDictionary } from '../../../util/vs/base/common/collections'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { byokKnownModelToAPIInfo, resolveModelInfo } from '../common/byokProvider'; import { OpenAIEndpoint } from '../node/openAIEndpoint'; import { AbstractOpenAICompatibleLMProvider, LanguageModelChatConfiguration, OpenAICompatibleLanguageModelChatInformation } from './abstractLanguageModelChatProvider'; import { IBYOKStorageService } from './byokStorageService'; export function resolveCustomOAIUrl(modelId: string, url: string): string { // The fully resolved url was already passed in if (hasExplicitApiPath(url)) { return url; } // Remove the trailing slash if (url.endsWith('/')) { url = url.slice(0, -1); } // Default to chat completions for base URLs const defaultApiPath = '/chat/completions'; // Check if URL already contains any version pattern like /v1, /v2, etc const versionPattern = /\/v\d+$/; if (versionPattern.test(url)) { return `${url}${defaultApiPath}`; } // For standard OpenAI-compatible endpoints, just append the standard path return `${url}/v1${defaultApiPath}`; } export function hasExplicitApiPath(url: string): boolean { return url.includes('/responses') || url.includes('/chat/completions'); } export interface CustomOAIModelProviderConfig extends LanguageModelChatConfiguration { url?: string; models?: CustomOAIModelConfig[]; } interface _CustomOAIModelConfig { name: string; url: string; maxInputTokens: number; maxOutputTokens: number; toolCalling: boolean; vision: boolean; thinking?: boolean; streaming?: boolean; editTools?: EndpointEditToolName[]; requestHeaders?: Record; zeroDataRetentionEnabled?: boolean; } export interface CustomOAIModelConfig extends _CustomOAIModelConfig { id: string; } export abstract class AbstractCustomOAIBYOKModelProvider extends AbstractOpenAICompatibleLMProvider { constructor( id: string, name: string, byokStorageService: IBYOKStorageService, @ILogService logService: ILogService, @IFetcherService fetcherService: IFetcherService, @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @IExperimentationService expService: IExperimentationService, @IVSCodeExtensionContext private readonly _extensionContext: IVSCodeExtensionContext ) { super(id, name, undefined, byokStorageService, fetcherService, logService, instantiationService, configurationService, expService); } protected async migrateConfig(configKey: Config>, providerName: string, providerGroupName: string): Promise { // Check if migration has already been completed const migrationKey = `copilot-byok-migration-${providerName}-${configKey}`; const migrationCompleted = this._extensionContext.globalState.get(migrationKey, false); if (migrationCompleted) { return; } const customOAIModelConfigsByApiKey: Map> = new Map(); const customOAIModelProviderConfig = this._configurationService.getConfig>(configKey); for (const [modelId, modelConfig] of Object.entries(customOAIModelProviderConfig)) { const apiKey = await this._byokStorageService.getAPIKey(providerName, modelId) ?? ''; const customOAIModelConfigs = customOAIModelConfigsByApiKey.get(apiKey) ?? []; customOAIModelConfigs.push({ ...modelConfig, id: modelId, requiresAPIKey: undefined }); customOAIModelConfigsByApiKey.set(apiKey, customOAIModelConfigs); } if (customOAIModelConfigsByApiKey.size > 0) { for (const [apiKey, customOAIModelConfigs] of customOAIModelConfigsByApiKey.entries()) { await this.configureDefaultGroupIfExists(providerGroupName, { models: customOAIModelConfigs, apiKey: apiKey || undefined }); } // Mark migration as completed instead of deleting the config await this._extensionContext.globalState.update(migrationKey, true); } } protected override async configureDefaultGroupWithApiKeyOnly(): Promise { // No-op: Custom OAI models are configured separately via migration return; } protected override async getAllModels(silent: boolean, apiKey: string | undefined, configuration: CustomOAIModelProviderConfig | undefined): Promise[]> { if (configuration?.url) { return super.getAllModels(silent, apiKey, configuration); } const models: OpenAICompatibleLanguageModelChatInformation[] = []; if (Array.isArray(configuration?.models)) { for (const modelConfig of configuration.models) { models.push({ ...byokKnownModelToAPIInfo(this._name, modelConfig.id, modelConfig), url: modelConfig.url }); } } return models; } protected override async createOpenAIEndPoint(model: OpenAICompatibleLanguageModelChatInformation): Promise { const url = this.resolveUrl(model.id, model.url); const modelConfiguration = model.configuration?.models?.find(m => m.id === model.id); const modelCapabilities = { maxInputTokens: model.maxInputTokens, maxOutputTokens: model.maxOutputTokens, toolCalling: !!model.capabilities?.toolCalling || false, vision: !!model.capabilities?.imageInput || false, name: model.name, url, thinking: modelConfiguration?.thinking ?? false, streaming: modelConfiguration?.streaming, requestHeaders: modelConfiguration?.requestHeaders, zeroDataRetentionEnabled: modelConfiguration?.zeroDataRetentionEnabled }; const modelInfo = resolveModelInfo(model.id, this._name, undefined, modelCapabilities); if (modelCapabilities?.url?.includes('/responses')) { modelInfo.supported_endpoints = [ ModelSupportedEndpoint.ChatCompletions, ModelSupportedEndpoint.Responses ]; } return this._instantiationService.createInstance(OpenAIEndpoint, modelInfo, model.configuration?.apiKey ?? '', url); } protected getModelsBaseUrl(configuration: CustomOAIModelProviderConfig | undefined): string | undefined { return configuration?.url; } protected abstract resolveUrl(modelId: string, url: string): string; } export class CustomOAIBYOKModelProvider extends AbstractCustomOAIBYOKModelProvider { static readonly providerName: string = 'CustomOAI'; private providerName: string = CustomOAIBYOKModelProvider.providerName; constructor( _byokStorageService: IBYOKStorageService, @ILogService logService: ILogService, @IFetcherService fetcherService: IFetcherService, @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @IExperimentationService expService: IExperimentationService, @IVSCodeExtensionContext extensionContext: IVSCodeExtensionContext ) { super(CustomOAIBYOKModelProvider.providerName.toLowerCase(), CustomOAIBYOKModelProvider.providerName, _byokStorageService, logService, fetcherService, instantiationService, configurationService, expService, extensionContext); this.migrateExistingConfigs(); } // TODO: Remove this after 6 months private async migrateExistingConfigs(): Promise { await this.migrateConfig(ConfigKey.Deprecated.CustomOAIModels, this.providerName, this.providerName); } protected resolveUrl(modelId: string, url: string): string { return resolveCustomOAIUrl(modelId, url); } } ================================================ FILE: src/extension/byok/vscode-node/geminiNativeProvider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ApiError, GenerateContentParameters, GoogleGenAI, Tool, Type } from '@google/genai'; import { CancellationToken, LanguageModelChatInformation, LanguageModelChatMessage, LanguageModelChatMessage2, LanguageModelResponsePart2, LanguageModelTextPart, LanguageModelThinkingPart, LanguageModelToolCallPart, Progress, ProvideLanguageModelChatResponseOptions } from 'vscode'; import { ChatFetchResponseType, ChatLocation } from '../../../platform/chat/common/commonTypes'; import { ILogService } from '../../../platform/log/common/logService'; import { IResponseDelta, OpenAiFunctionTool } from '../../../platform/networking/common/fetch'; import { APIUsage } from '../../../platform/networking/common/openai'; import { CopilotChatAttr, emitInferenceDetailsEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, type OTelModelOptions, StdAttr, truncateForOTel } from '../../../platform/otel/common/index'; import { IOTelService, SpanKind, SpanStatusCode } from '../../../platform/otel/common/otelService'; import { IRequestLogger, retrieveCapturingTokenByCorrelation, runWithCapturingToken } from '../../../platform/requestLogger/node/requestLogger'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; import { toErrorMessage } from '../../../util/common/errorMessage'; import { RecordedProgress } from '../../../util/common/progressRecorder'; import { generateUuid } from '../../../util/vs/base/common/uuid'; import { BYOKKnownModels, byokKnownModelsToAPIInfo, BYOKModelCapabilities, LMResponsePart } from '../common/byokProvider'; import { toGeminiFunction as toGeminiFunctionDeclaration, ToolJsonSchema } from '../common/geminiFunctionDeclarationConverter'; import { apiMessageToGeminiMessage, geminiMessagesToRawMessagesForLogging } from '../common/geminiMessageConverter'; import { AbstractLanguageModelChatProvider, ExtendedLanguageModelChatInformation, LanguageModelChatConfiguration } from './abstractLanguageModelChatProvider'; import { IBYOKStorageService } from './byokStorageService'; export class GeminiNativeBYOKLMProvider extends AbstractLanguageModelChatProvider { public static readonly providerName = 'Gemini'; constructor( knownModels: BYOKKnownModels | undefined, byokStorageService: IBYOKStorageService, @ILogService logService: ILogService, @IRequestLogger private readonly _requestLogger: IRequestLogger, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IOTelService private readonly _otelService: IOTelService, ) { super(GeminiNativeBYOKLMProvider.providerName.toLowerCase(), GeminiNativeBYOKLMProvider.providerName, knownModels, byokStorageService, logService); } protected async getAllModels(silent: boolean, apiKey: string | undefined): Promise[]> { if (!apiKey && silent) { return []; } try { const client = new GoogleGenAI({ apiKey }); const models = await client.models.list(); const modelList: Record = {}; for await (const model of models) { const modelId = model.name; if (!modelId) { continue; // Skip models without names } // Enable only known models. if (this._knownModels && this._knownModels[modelId]) { modelList[modelId] = this._knownModels[modelId]; } } return byokKnownModelsToAPIInfo(this._name, modelList); } catch (e) { let error: Error; if (e instanceof ApiError) { let message = e.message; try { message = JSON.parse(message).error?.message; } catch { /* ignore */ } error = new Error(message ?? e.message, { cause: e }); } else { error = new Error(toErrorMessage(e, true)); } this._logService.error(error, `Error fetching available ${GeminiNativeBYOKLMProvider.providerName} models`); throw error; } } async provideLanguageModelChatResponse(model: ExtendedLanguageModelChatInformation, messages: Array, options: ProvideLanguageModelChatResponseOptions, progress: Progress, token: CancellationToken): Promise { // Restore CapturingToken context if correlation ID was passed through modelOptions. // This handles the case where AsyncLocalStorage context was lost crossing VS Code IPC. const correlationId = (options as { modelOptions?: OTelModelOptions }).modelOptions?._capturingTokenCorrelationId; const capturingToken = correlationId ? retrieveCapturingTokenByCorrelation(correlationId) : undefined; // Restore OTel trace context to link spans back to the agent trace const parentTraceContext = (options as { modelOptions?: OTelModelOptions }).modelOptions?._otelTraceContext ?? undefined; // OTel span handle — created outside doRequest, enriched inside with usage data let otelSpan: ReturnType | undefined; const doRequest = async () => { const issuedTime = Date.now(); const apiKey = model.configuration?.apiKey; if (!apiKey) { throw new Error('API key not found for the model'); } const client = new GoogleGenAI({ apiKey }); // Convert the messages from the API format into messages that we can use against Gemini const { contents, systemInstruction } = apiMessageToGeminiMessage(messages as LanguageModelChatMessage[]); const requestId = generateUuid(); const pendingLoggedChatRequest = this._requestLogger.logChatRequest( 'GeminiNativeBYOK', { model: model.id, modelMaxPromptTokens: model.maxInputTokens, urlOrRequestMetadata: 'https://generativelanguage.googleapis.com', }, { model: model.id, messages: geminiMessagesToRawMessagesForLogging(contents, systemInstruction), ourRequestId: requestId, location: ChatLocation.Other, body: { tools: options.tools?.map((tool): OpenAiFunctionTool => ({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.inputSchema } })) } }); // Convert VS Code tools to Gemini function declarations const tools: Tool[] = (options.tools ?? []).length > 0 ? [{ functionDeclarations: (options.tools ?? []).map(tool => { if (!tool.inputSchema) { return { name: tool.name, description: tool.description, parameters: { type: Type.OBJECT, properties: {}, required: [] } }; } // Transform the input schema to match Gemini's expectations const finalTool = toGeminiFunctionDeclaration(tool.name, tool.description, tool.inputSchema as ToolJsonSchema); finalTool.description = tool.description || finalTool.description; return finalTool; }) }] : []; // Bridge VS Code cancellation token to Gemini abortSignal for early network termination const abortController = new AbortController(); const cancelSub = token.onCancellationRequested(() => { abortController.abort(); this._logService.trace('Gemini request aborted via VS Code cancellation token'); }); const params: GenerateContentParameters = { model: model.id, contents: contents, config: { systemInstruction: systemInstruction, tools: tools.length > 0 ? tools : undefined, maxOutputTokens: model.maxOutputTokens, thinkingConfig: { includeThoughts: true, }, abortSignal: abortController.signal } }; const wrappedProgress = new RecordedProgress(progress); try { const result = await this._makeRequest(client, wrappedProgress, params, token, issuedTime); if (result.ttft) { pendingLoggedChatRequest.markTimeToFirstToken(result.ttft); } pendingLoggedChatRequest.resolve({ type: ChatFetchResponseType.Success, requestId, serverRequestId: requestId, usage: result.usage, resolvedModel: model.id, value: ['value'], }, wrappedProgress.items.map((i): IResponseDelta => { return { text: i instanceof LanguageModelTextPart ? i.value : '', copilotToolCalls: i instanceof LanguageModelToolCallPart ? [{ name: i.name, arguments: JSON.stringify(i.input), id: i.callId }] : undefined, }; })); // Enrich OTel span with usage data from the Gemini response if (otelSpan && result.usage) { otelSpan.setAttributes({ [GenAiAttr.USAGE_INPUT_TOKENS]: result.usage.prompt_tokens ?? 0, [GenAiAttr.USAGE_OUTPUT_TOKENS]: result.usage.completion_tokens ?? 0, ...(result.usage.prompt_tokens_details?.cached_tokens ? { [GenAiAttr.USAGE_CACHE_READ_INPUT_TOKENS]: result.usage.prompt_tokens_details.cached_tokens } : {}), [GenAiAttr.RESPONSE_MODEL]: model.id, [GenAiAttr.RESPONSE_ID]: requestId, [GenAiAttr.RESPONSE_FINISH_REASONS]: ['stop'], [GenAiAttr.CONVERSATION_ID]: requestId, ...(result.ttft ? { [CopilotChatAttr.TIME_TO_FIRST_TOKEN]: result.ttft } : {}), [GenAiAttr.REQUEST_MAX_TOKENS]: model.maxOutputTokens ?? 0, }); // Opt-in content capture if (this._otelService.config.captureContent) { const responseText = wrappedProgress.items .filter((p): p is LanguageModelTextPart => p instanceof LanguageModelTextPart) .map(p => p.value).join(''); const toolCalls = wrappedProgress.items .filter((p): p is LanguageModelToolCallPart => p instanceof LanguageModelToolCallPart) .map(tc => ({ type: 'tool_call' as const, id: tc.callId, name: tc.name, arguments: tc.input })); const parts: Array<{ type: string; content?: string; id?: string; name?: string; arguments?: unknown }> = []; if (responseText) { parts.push({ type: 'text', content: responseText }); } parts.push(...toolCalls); if (parts.length > 0) { otelSpan.setAttribute(GenAiAttr.OUTPUT_MESSAGES, truncateForOTel(JSON.stringify([{ role: 'assistant', parts }]))); } } } // Record OTel metrics for this Gemini LLM call if (result.usage) { const durationSec = (Date.now() - issuedTime) / 1000; const metricAttrs = { operationName: GenAiOperationName.CHAT, providerName: 'gemini', requestModel: model.id, responseModel: model.id }; GenAiMetrics.recordOperationDuration(this._otelService, durationSec, metricAttrs); if (result.usage.prompt_tokens) { GenAiMetrics.recordTokenUsage(this._otelService, result.usage.prompt_tokens, 'input', metricAttrs); } if (result.usage.completion_tokens) { GenAiMetrics.recordTokenUsage(this._otelService, result.usage.completion_tokens, 'output', metricAttrs); } if (result.ttft) { GenAiMetrics.recordTimeToFirstToken(this._otelService, model.id, result.ttft / 1000); } } // Emit OTel inference details event emitInferenceDetailsEvent( this._otelService, { model: model.id, maxTokens: model.maxOutputTokens }, result.usage ? { id: requestId, model: model.id, finishReasons: ['stop'], inputTokens: result.usage.prompt_tokens, outputTokens: result.usage.completion_tokens, } : undefined, ); // Send success telemetry matching response.success format /* __GDPR__ "response.success" : { "owner": "digitarald", "comment": "Report quality details for a successful service response.", "reason": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Reason for why a response finished" }, "filterReason": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Reason for why a response was filtered" }, "source": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Source of the initial request" }, "initiatorType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the request was initiated by a user or an agent" }, "model": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Model selection for the response" }, "modelInvoked": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Actual model invoked for the response" }, "apiType": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "API type for the response- chat completions or responses" }, "requestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Id of the current turn request" }, "gitHubRequestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "GitHub request id if available" }, "associatedRequestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Another request ID that this request is associated with (eg, the originating request of a summarization request)." }, "reasoningEffort": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Reasoning effort level" }, "reasoningSummary": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Reasoning summary level" }, "fetcher": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The fetcher used for the request" }, "transport": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The transport used for the request (http or websocket)" }, "totalTokenMax": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Maximum total token window", "isMeasurement": true }, "clientPromptTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of prompt tokens, locally counted", "isMeasurement": true }, "promptTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of prompt tokens, server side counted", "isMeasurement": true }, "promptCacheTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of prompt tokens hitting cache as reported by server", "isMeasurement": true }, "tokenCountMax": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Maximum generated tokens", "isMeasurement": true }, "tokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of generated tokens", "isMeasurement": true }, "reasoningTokens": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of reasoning tokens", "isMeasurement": true }, "acceptedPredictionTokens": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of tokens in the prediction that appeared in the completion", "isMeasurement": true }, "rejectedPredictionTokens": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of tokens in the prediction that appeared in the completion", "isMeasurement": true }, "completionTokens": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Number of tokens in the output", "isMeasurement": true }, "timeToFirstToken": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Time to first token", "isMeasurement": true }, "timeToFirstTokenEmitted": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Time to first token emitted (visible text)", "isMeasurement": true }, "timeToComplete": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Time to complete the request", "isMeasurement": true }, "issuedTime": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Timestamp when the request was issued", "isMeasurement": true }, "isVisionRequest": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether the request was for a vision model", "isMeasurement": true }, "isBYOK": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the request was for a BYOK model", "isMeasurement": true }, "isAuto": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the request was for an Auto model", "isMeasurement": true }, "bytesReceived": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of bytes received in the response", "isMeasurement": true }, "retryAfterError": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Error of the original request." }, "retryAfterErrorGitHubRequestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "GitHub request id of the original request if available" }, "connectivityTestError": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Error of the connectivity test." }, "connectivityTestErrorGitHubRequestId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "GitHub request id of the connectivity test request if available" }, "retryAfterFilterCategory": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "If the response was filtered and this is a retry attempt, this contains the original filtered content category." }, "suspendEventSeen": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether a system suspend event was seen during the request", "isMeasurement": true }, "resumeEventSeen": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether a system resume event was seen during the request", "isMeasurement": true } } */ this._telemetryService.sendTelemetryEvent('response.success', { github: true, microsoft: true }, { source: 'byok.gemini', model: model.id, requestId, }, { totalTokenMax: model.maxInputTokens ?? -1, tokenCountMax: model.maxOutputTokens ?? -1, promptTokenCount: result.usage?.prompt_tokens, promptCacheTokenCount: result.usage?.prompt_tokens_details?.cached_tokens, tokenCount: result.usage?.total_tokens, completionTokens: result.usage?.completion_tokens, timeToFirstToken: result.ttft, timeToFirstTokenEmitted: result.ttfte, timeToComplete: Date.now() - issuedTime, issuedTime, isBYOK: 1, }); } catch (err) { this._logService.error(`BYOK GeminiNative error: ${toErrorMessage(err, true)}`); pendingLoggedChatRequest.resolve({ type: token.isCancellationRequested ? ChatFetchResponseType.Canceled : ChatFetchResponseType.Unknown, requestId, serverRequestId: requestId, reason: token.isCancellationRequested ? 'cancelled' : toErrorMessage(err) }, wrappedProgress.items.map((i): IResponseDelta => { return { text: i instanceof LanguageModelTextPart ? i.value : '', copilotToolCalls: i instanceof LanguageModelToolCallPart ? [{ name: i.name, arguments: JSON.stringify(i.input), id: i.callId }] : undefined, }; })); throw err; } finally { cancelSub.dispose(); } }; // Create OTel span and execute with trace context + CapturingToken const executeRequest = async () => { otelSpan = this._otelService.startSpan(`chat ${model.id}`, { kind: SpanKind.CLIENT, attributes: { [GenAiAttr.OPERATION_NAME]: GenAiOperationName.CHAT, [GenAiAttr.PROVIDER_NAME]: 'gemini', [GenAiAttr.REQUEST_MODEL]: model.id, [GenAiAttr.AGENT_NAME]: 'GeminiBYOK', [CopilotChatAttr.MAX_PROMPT_TOKENS]: model.maxInputTokens, [StdAttr.SERVER_ADDRESS]: 'generativelanguage.googleapis.com', }, }); // Opt-in: capture input messages if (this._otelService.config.captureContent) { try { const roleNames: Record = { 1: 'user', 2: 'assistant', 3: 'system' }; const inputMsgs = messages.map(m => { const msg = m as LanguageModelChatMessage; const role = roleNames[msg.role] ?? String(msg.role); const textParts: string[] = []; if (Array.isArray(msg.content)) { for (const p of msg.content) { if (p instanceof LanguageModelTextPart) { textParts.push(p.value); } } } const content = textParts.length > 0 ? textParts.join('') : '[non-text content]'; return { role, parts: [{ type: 'text', content }] }; }); otelSpan.setAttribute(GenAiAttr.INPUT_MESSAGES, truncateForOTel(JSON.stringify(inputMsgs))); } catch { /* swallow */ } } try { const result = capturingToken ? await runWithCapturingToken(capturingToken, doRequest) : await doRequest(); otelSpan.setStatus(SpanStatusCode.OK); return result; } catch (err) { otelSpan.setStatus(SpanStatusCode.ERROR, err instanceof Error ? err.message : String(err)); throw err; } finally { otelSpan.end(); } }; if (parentTraceContext) { return this._otelService.runWithTraceContext(parentTraceContext, executeRequest); } return executeRequest(); } async provideTokenCount(model: LanguageModelChatInformation, text: string | LanguageModelChatMessage | LanguageModelChatMessage2, token: CancellationToken): Promise { // Simple estimation for approximate token count - actual token count would require Gemini's tokenizer return Math.ceil(text.toString().length / 4); } private async _makeRequest(client: GoogleGenAI, progress: Progress, params: GenerateContentParameters, token: CancellationToken, issuedTime: number): Promise<{ ttft: number | undefined; ttfte: number | undefined; usage: APIUsage | undefined }> { const start = Date.now(); let ttft: number | undefined; let ttfte: number | undefined; let usage: APIUsage | undefined; try { const stream = await client.models.generateContentStream(params); let pendingThinkingSignature: string | undefined; for await (const chunk of stream) { if (token.isCancellationRequested) { break; } if (ttft === undefined) { ttft = Date.now() - start; } this._logService.trace(`Gemini chunk: ${JSON.stringify(chunk)}`); // Process the streaming response chunks if (chunk.candidates && chunk.candidates.length > 0) { // choose the primary candidate const candidate = chunk.candidates[0]; if (candidate.content && candidate.content.parts) { for (const part of candidate.content.parts) { // First, capture thought signature from this part (if present) if ('thoughtSignature' in part && part.thoughtSignature) { pendingThinkingSignature = part.thoughtSignature as string; } // Now handle the actual content parts if ('thought' in part && part.thought === true && part.text) { // Handle thinking/reasoning content from Gemini API if (ttfte === undefined) { ttfte = Date.now() - issuedTime; } progress.report(new LanguageModelThinkingPart(part.text)); } else if (part.text) { if (ttfte === undefined) { ttfte = Date.now() - issuedTime; } progress.report(new LanguageModelTextPart(part.text)); } else if (part.functionCall && part.functionCall.name) { // Gemini 3 includes thought signatures for function calling // If we have a pending signature, emit it as a thinking part with metadata.signature if (pendingThinkingSignature) { const thinkingPart = new LanguageModelThinkingPart('', undefined, { signature: pendingThinkingSignature }); progress.report(thinkingPart); pendingThinkingSignature = undefined; } if (ttfte === undefined) { ttfte = Date.now() - issuedTime; } progress.report(new LanguageModelToolCallPart( generateUuid(), part.functionCall.name, part.functionCall.args || {} )); } } } } // Extract usage information if available in the chunk // Initialize on first chunk with usageMetadata, then update incrementally // This ensures we capture prompt token info even if stream is cancelled mid-way if (chunk.usageMetadata) { const promptTokens = chunk.usageMetadata.promptTokenCount; // For thinking models (e.g., gemini-3-pro-high), candidatesTokenCount only includes // regular output tokens. thoughtsTokenCount contains the thinking/reasoning tokens. // We include both in the completion token count. const candidateTokens = chunk.usageMetadata.candidatesTokenCount ?? 0; const thoughtTokens = chunk.usageMetadata.thoughtsTokenCount ?? 0; const completionTokens = candidateTokens + thoughtTokens > 0 ? candidateTokens + thoughtTokens : undefined; const cachedTokens = chunk.usageMetadata.cachedContentTokenCount; if (!usage) { // Initialize usage on first chunk - use -1 as sentinel for unavailable values usage = { completion_tokens: completionTokens ?? -1, prompt_tokens: promptTokens ?? -1, total_tokens: chunk.usageMetadata.totalTokenCount ?? -1, prompt_tokens_details: { cached_tokens: cachedTokens ?? 0, } }; } else { // Update with latest values, preserving existing non-sentinel values if (promptTokens !== undefined) { usage.prompt_tokens = promptTokens; } if (completionTokens !== undefined) { usage.completion_tokens = completionTokens; } if (chunk.usageMetadata.totalTokenCount !== undefined) { usage.total_tokens = chunk.usageMetadata.totalTokenCount; } else if (usage.prompt_tokens !== -1 && usage.completion_tokens !== -1) { usage.total_tokens = usage.prompt_tokens + usage.completion_tokens; } if (cachedTokens !== undefined) { usage.prompt_tokens_details!.cached_tokens = cachedTokens; } } } } return { ttft, ttfte, usage }; } catch (error) { if ((error as any)?.name === 'AbortError' || token.isCancellationRequested) { this._logService.trace('Gemini streaming aborted'); // Return partial usage data collected before cancellation return { ttft, ttfte, usage }; } this._logService.error(`Gemini streaming error: ${toErrorMessage(error, true)}`); throw error; } } } ================================================ FILE: src/extension/byok/vscode-node/ollamaProvider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { IChatModelInformation } from '../../../platform/endpoint/common/endpointProvider'; import { ILogService } from '../../../platform/log/common/logService'; import { IFetcherService } from '../../../platform/networking/common/fetcherService'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { ErrorUtils } from '../../../util/common/errors'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { byokKnownModelsToAPIInfo, resolveModelInfo } from '../common/byokProvider'; import { OpenAIEndpoint } from '../node/openAIEndpoint'; import { AbstractOpenAICompatibleLMProvider, LanguageModelChatConfiguration, OpenAICompatibleLanguageModelChatInformation } from './abstractLanguageModelChatProvider'; import { IBYOKStorageService } from './byokStorageService'; interface OllamaModelInfoAPIResponse { template: string; capabilities: string[]; details: { family: string }; remote_model?: string; model_info?: { 'general.basename': string; 'general.architecture': string; [other: string]: any; }; } interface OllamaVersionResponse { version: string; } // Minimum supported Ollama version - versions below this may have compatibility issues const MINIMUM_OLLAMA_VERSION = '0.6.4'; export interface OllamaConfig extends LanguageModelChatConfiguration { url: string; } export class OllamaLMProvider extends AbstractOpenAICompatibleLMProvider { public static readonly providerName = 'Ollama'; private _modelCache = new Map(); constructor( byokStorageService: IBYOKStorageService, @IFetcherService fetcherService: IFetcherService, @IConfigurationService configurationService: IConfigurationService, @ILogService logService: ILogService, @IInstantiationService instantiationService: IInstantiationService, @IExperimentationService expService: IExperimentationService ) { super( OllamaLMProvider.providerName.toLowerCase(), OllamaLMProvider.providerName, undefined, byokStorageService, fetcherService, logService, instantiationService, configurationService, expService ); this.migrateConfig(); } private async migrateConfig(): Promise { const baseUrl = this.getBaseUrlFromSettings(); if (!baseUrl) { return; } await this.configureDefaultGroupIfExists(this._name, { url: baseUrl }); await this._configurationService.setConfig(ConfigKey.Deprecated.OllamaEndpoint, undefined); } private getBaseUrlFromSettings(): string | undefined { if (this._configurationService.isConfigured(ConfigKey.Deprecated.OllamaEndpoint)) { return this._configurationService.getConfig(ConfigKey.Deprecated.OllamaEndpoint); } return undefined; } protected override async getAllModels(silent: boolean, apiKey: string | undefined, config: OllamaConfig | undefined): Promise[]> { if (!config) { return []; } const ollamaBaseUrl = config.url; try { // Check Ollama server version before proceeding with model operations await this._checkOllamaVersion(ollamaBaseUrl); const response = await this._fetcherService.fetch(`${ollamaBaseUrl}/api/tags`, { method: 'GET', callSite: 'ollama-tags' }); const models = (await response.json()).models; this._knownModels = {}; for (const model of models) { let modelInfo = this._modelCache.get(`${ollamaBaseUrl}/${model.model}`); if (!modelInfo) { try { modelInfo = await this._getOllamaModelInfo(ollamaBaseUrl, model.model); } catch (e) { const error = ErrorUtils.fromUnknown(e); this._logService.error(error, 'ollamaProvider: failed to fetch Ollama model info'); this._logService.debug(`[ollamaProvider] Failed model info fetch for model=${model.model}`); continue; // Skip this model but continue processing others } this._modelCache.set(`${ollamaBaseUrl}/${model.model}`, modelInfo); } this._knownModels[modelInfo.id] = { maxInputTokens: modelInfo.capabilities.limits?.max_prompt_tokens ?? 4096, maxOutputTokens: modelInfo.capabilities.limits?.max_output_tokens ?? 4096, name: modelInfo.name, toolCalling: !!modelInfo.capabilities.supports.tool_calls, vision: !!modelInfo.capabilities.supports.vision }; } return byokKnownModelsToAPIInfo(this._name, this._knownModels).map(model => ({ ...model, url: ollamaBaseUrl })); } catch (e) { // Check if this is our version check error and preserve it if (e instanceof Error && e.message.includes('Ollama server version')) { throw e; } throw new Error('Failed to fetch models from Ollama. Please ensure Ollama is running. If ollama is on another host, please configure the `"github.copilot.chat.byok.ollamaEndpoint"` setting.'); } } protected override getModelsBaseUrl(configuration: OllamaConfig | undefined): string { return configuration?.url ?? 'http://localhost:11434'; } protected override async createOpenAIEndPoint(model: OpenAICompatibleLanguageModelChatInformation): Promise { const modelInfo = this.getModelInfo(model.id, model.url); const url = `${model.url}/v1/chat/completions`; return this._instantiationService.createInstance(OpenAIEndpoint, modelInfo, model.configuration?.apiKey ?? '', url); } private async _getOllamaModelInfo(ollamaBaseUrl: string, modelId: string): Promise { const modelInfo = await this._fetchOllamaModelInformation(ollamaBaseUrl, modelId); const contextWindow = modelInfo?.model_info?.[`${modelInfo.model_info['general.architecture']}.context_length`] ?? 32768; const outputTokens = contextWindow < 4096 ? Math.floor(contextWindow / 2) : 4096; const modelCapabilities = { name: modelInfo?.model_info?.['general.basename'] ?? modelInfo.remote_model ?? modelId, maxOutputTokens: outputTokens, maxInputTokens: contextWindow - outputTokens, vision: modelInfo.capabilities.includes('vision'), toolCalling: modelInfo.capabilities.includes('tools') }; return resolveModelInfo(modelId, this._name, this._knownModels, modelCapabilities); } /** * Compare version strings to check if current version meets minimum requirements * @param currentVersion Current Ollama server version * @returns true if version is supported, false otherwise */ private _isVersionSupported(currentVersion: string): boolean { if (currentVersion === '0.0.0') { // allow all dev versions through return true; } // Simple version comparison: split by dots and compare numerically const currentParts = currentVersion.split('.').map(n => parseInt(n, 10)); const minimumParts = MINIMUM_OLLAMA_VERSION.split('.').map(n => parseInt(n, 10)); for (let i = 0; i < Math.max(currentParts.length, minimumParts.length); i++) { const current = currentParts[i] || 0; const minimum = minimumParts[i] || 0; if (current > minimum) { return true; } if (current < minimum) { return false; } } return true; // versions are equal } private async _fetchOllamaModelInformation(ollamaBaseUrl: string, modelId: string): Promise { const response = await this._fetcherService.fetch(`${ollamaBaseUrl}/api/show`, { method: 'POST', callSite: 'ollama-show', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: modelId }) }); return response.json() as unknown as OllamaModelInfoAPIResponse; } /** * Check if the connected Ollama server version meets the minimum requirements * @throws Error if version is below minimum or version check fails */ private async _checkOllamaVersion(ollamaBaseUrl: string): Promise { try { const response = await this._fetcherService.fetch(`${ollamaBaseUrl}/api/version`, { method: 'GET', callSite: 'ollama-version' }); const versionInfo = await response.json() as OllamaVersionResponse; if (!this._isVersionSupported(versionInfo.version)) { throw new Error( `Ollama server version ${versionInfo.version} is not supported. ` + `Please upgrade to version ${MINIMUM_OLLAMA_VERSION} or higher. ` + `Visit https://ollama.ai for upgrade instructions.` ); } } catch (e) { if (e instanceof Error && e.message.includes('Ollama server version')) { // Re-throw our custom version error throw e; } // If version endpoint fails throw new Error( `Unable to verify Ollama server version. Please ensure you have Ollama version ${MINIMUM_OLLAMA_VERSION} or higher installed. ` + `If you're running an older version, please upgrade from https://ollama.ai` ); } } } ================================================ FILE: src/extension/byok/vscode-node/openAIProvider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { IChatModelInformation, ModelSupportedEndpoint } from '../../../platform/endpoint/common/endpointProvider'; import { ILogService } from '../../../platform/log/common/logService'; import { IFetcherService } from '../../../platform/networking/common/fetcherService'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { BYOKKnownModels } from '../common/byokProvider'; import { AbstractOpenAICompatibleLMProvider } from './abstractLanguageModelChatProvider'; import { IBYOKStorageService } from './byokStorageService'; export class OAIBYOKLMProvider extends AbstractOpenAICompatibleLMProvider { public static readonly providerName = 'OpenAI'; constructor( knownModels: BYOKKnownModels, byokStorageService: IBYOKStorageService, @IFetcherService fetcherService: IFetcherService, @ILogService logService: ILogService, @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @IExperimentationService expService: IExperimentationService ) { super( OAIBYOKLMProvider.providerName.toLowerCase(), OAIBYOKLMProvider.providerName, knownModels, byokStorageService, fetcherService, logService, instantiationService, configurationService, expService ); } protected override getModelsBaseUrl(): string { return 'https://api.openai.com/v1'; } protected override getModelInfo(modelId: string, modelUrl: string): IChatModelInformation { const modelInfo = super.getModelInfo(modelId, modelUrl); modelInfo.supported_endpoints = [ ModelSupportedEndpoint.ChatCompletions, ModelSupportedEndpoint.Responses ]; return modelInfo; } } ================================================ FILE: src/extension/byok/vscode-node/openRouterProvider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { ILogService } from '../../../platform/log/common/logService'; import { IFetcherService } from '../../../platform/networking/common/fetcherService'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { BYOKModelCapabilities } from '../common/byokProvider'; import { AbstractOpenAICompatibleLMProvider } from './abstractLanguageModelChatProvider'; import { IBYOKStorageService } from './byokStorageService'; interface OpenRouterModelData { id: string; name: string; supported_parameters?: string[]; architecture?: { input_modalities?: string[]; }; top_provider: { context_length: number; }; } export class OpenRouterLMProvider extends AbstractOpenAICompatibleLMProvider { public static readonly providerName = 'OpenRouter'; constructor( byokStorageService: IBYOKStorageService, @IFetcherService fetcherService: IFetcherService, @ILogService logService: ILogService, @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @IExperimentationService expService: IExperimentationService ) { super( OpenRouterLMProvider.providerName.toLowerCase(), OpenRouterLMProvider.providerName, undefined, byokStorageService, fetcherService, logService, instantiationService, configurationService, expService ); } protected override getModelsBaseUrl(): string | undefined { return 'https://openrouter.ai/api/v1'; } protected override getModelsDiscoveryUrl(modelsBaseUrl: string): string { return `${modelsBaseUrl}/models?supported_parameters=tools`; } protected override resolveModelCapabilities(modelData: unknown): BYOKModelCapabilities | undefined { const openRouterModelData = modelData as OpenRouterModelData; return { name: openRouterModelData.name, toolCalling: openRouterModelData.supported_parameters?.includes('tools') ?? false, vision: openRouterModelData.architecture?.input_modalities?.includes('image') ?? false, maxInputTokens: openRouterModelData.top_provider.context_length - 16000, maxOutputTokens: 16000 }; } } ================================================ FILE: src/extension/byok/vscode-node/test/azureProvider.spec.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { BlockedExtensionService, IBlockedExtensionService } from '../../../../platform/chat/common/blockedExtensionService'; import { DisposableStore } from '../../../../util/vs/base/common/lifecycle'; import { SyncDescriptor } from '../../../../util/vs/platform/instantiation/common/descriptors'; import { createExtensionUnitTestingServices } from '../../../test/node/services'; import { resolveAzureUrl } from '../azureProvider'; describe('AzureBYOKModelProvider', () => { const disposables = new DisposableStore(); beforeEach(() => { const testingServiceCollection = createExtensionUnitTestingServices(); // Add IBlockedExtensionService which is required by CopilotLanguageModelWrapper testingServiceCollection.define(IBlockedExtensionService, new SyncDescriptor(BlockedExtensionService)); }); afterEach(() => { disposables.clear(); vi.restoreAllMocks(); }); describe('resolveAzureUrl', () => { it('should handle Azure AI Foundry (models.ai.azure.com) URLs', () => { const url = 'https://my-endpoint.models.ai.azure.com'; const result = resolveAzureUrl('gpt-4', url); expect(result).toBe('https://my-endpoint.models.ai.azure.com/v1/chat/completions'); }); it('should handle Azure ML (inference.ml.azure.com) URLs', () => { const url = 'https://my-endpoint.inference.ml.azure.com'; const result = resolveAzureUrl('gpt-4', url); expect(result).toBe('https://my-endpoint.inference.ml.azure.com/v1/chat/completions'); }); it('should handle Azure OpenAI (openai.azure.com) URLs with deployment name', () => { const url = 'https://my-resource.openai.azure.com'; const result = resolveAzureUrl('gpt-4-deployment', url); expect(result).toBe('https://my-resource.openai.azure.com/openai/deployments/gpt-4-deployment/chat/completions?api-version=2025-01-01-preview'); }); it('should return URL unchanged if it already has explicit API path', () => { const url = 'https://my-endpoint.example.com/v1/chat/completions'; const result = resolveAzureUrl('gpt-4', url); expect(result).toBe(url); }); it('should remove trailing slash before processing', () => { const url = 'https://my-endpoint.models.ai.azure.com/'; const result = resolveAzureUrl('gpt-4', url); expect(result).toBe('https://my-endpoint.models.ai.azure.com/v1/chat/completions'); }); it('should remove /v1 suffix before processing', () => { const url = 'https://my-endpoint.models.ai.azure.com/v1'; const result = resolveAzureUrl('gpt-4', url); expect(result).toBe('https://my-endpoint.models.ai.azure.com/v1/chat/completions'); }); it('should throw error for unrecognized Azure URL', () => { const url = 'https://unknown.example.com'; expect(() => resolveAzureUrl('gpt-4', url)).toThrow('Unrecognized Azure deployment URL'); }); }); }); ================================================ FILE: src/extension/byok/vscode-node/test/geminiNativeProvider.spec.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { beforeEach, describe, expect, it, vi } from 'vitest'; import * as vscode from 'vscode'; import { NoopOTelService, resolveOTelConfig } from '../../../../platform/otel/common/index'; import type { CapturingToken } from '../../../../platform/requestLogger/common/capturingToken'; import type { IRequestLogger } from '../../../../platform/requestLogger/node/requestLogger'; import { NullTelemetryService } from '../../../../platform/telemetry/common/nullTelemetryService'; import { TestLogService } from '../../../../platform/testing/common/testLogService'; import type { IBYOKStorageService } from '../byokStorageService'; const mockHandleAPIKeyUpdate = vi.fn(); vi.mock('@google/genai', () => { class MockGoogleGenAI { public static createdWithApiKeys: string[] = []; public static streamChunks: any[] = []; public static listModelsResult: AsyncIterable = (async function* () { })(); public readonly apiKey: string; public readonly models: { list: () => Promise>; generateContentStream: (params: unknown) => Promise>; }; constructor(opts: { apiKey: string }) { this.apiKey = opts.apiKey; MockGoogleGenAI.createdWithApiKeys.push(opts.apiKey); this.models = { list: async () => MockGoogleGenAI.listModelsResult, generateContentStream: async () => (async function* () { for (const c of MockGoogleGenAI.streamChunks) { yield c; } })() }; } } return { GoogleGenAI: MockGoogleGenAI, Type: { OBJECT: 'object' }, }; }); vi.mock('../../common/byokProvider', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, handleAPIKeyUpdate: mockHandleAPIKeyUpdate, }; }); type ProgressItem = vscode.LanguageModelResponsePart2; class TestProgress implements vscode.Progress { public readonly items: ProgressItem[] = []; report(value: ProgressItem): void { this.items.push(value); } } function createStorageService(overrides?: Partial): IBYOKStorageService { return { getAPIKey: vi.fn().mockResolvedValue(undefined), storeAPIKey: vi.fn().mockResolvedValue(undefined), deleteAPIKey: vi.fn().mockResolvedValue(undefined), getStoredModelConfigs: vi.fn().mockResolvedValue({}), saveModelConfig: vi.fn().mockResolvedValue(undefined), removeModelConfig: vi.fn().mockResolvedValue(undefined), ...overrides, }; } function createRequestLogger(): IRequestLogger { const didChangeEmitter = new vscode.EventEmitter(); return { _serviceBrand: undefined, promptRendererTracing: false, captureInvocation: async (_request: CapturingToken, fn: () => Promise) => fn(), logToolCall: () => undefined, logModelListCall: () => undefined, logChatRequest: () => ({ markTimeToFirstToken: () => undefined, resolveWithCancelation: () => undefined, resolve: () => undefined, }), addPromptTrace: () => undefined, addEntry: () => undefined, onDidChangeRequests: didChangeEmitter.event, getRequests: () => [], enableWorkspaceEditTracing: () => undefined, disableWorkspaceEditTracing: () => undefined, } as unknown as IRequestLogger; } describe('GeminiNativeBYOKLMProvider', () => { beforeEach(() => { vi.clearAllMocks(); }); it.skip('throws a clear error when no API key is configured (no silent return)', async () => { const { GeminiNativeBYOKLMProvider } = await import('../geminiNativeProvider'); const storage = createStorageService({ getAPIKey: vi.fn().mockResolvedValue(undefined) }); const provider = new GeminiNativeBYOKLMProvider(undefined, storage, new TestLogService(), createRequestLogger(), new NullTelemetryService(), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '1.0.0', sessionId: 'test' }))); const model: vscode.LanguageModelChatInformation = { id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash', family: 'Gemini', version: '1.0.0', maxInputTokens: 1000, maxOutputTokens: 1000, capabilities: { toolCalling: false, imageInput: false } }; const messages: vscode.LanguageModelChatMessage[] = [ new vscode.LanguageModelChatMessage(vscode.LanguageModelChatMessageRole.User, 'hello') ]; const tokenSource = new vscode.CancellationTokenSource(); const progress = new TestProgress(); await expect(provider.provideLanguageModelChatResponse( model, messages, { requestInitiator: 'test', tools: [], toolMode: vscode.LanguageModelChatToolMode.Auto }, progress, tokenSource.token )).rejects.toThrow(/No API key configured/i); }); // it.skip('initializes the Gemini client on API key update and can stream a response', async () => { // const { GeminiNativeBYOKLMProvider } = await import('../geminiNativeProvider'); // const genai = await import('@google/genai'); // const MockGoogleGenAI = genai.GoogleGenAI as unknown as { createdWithApiKeys: string[]; streamChunks: any[] }; // MockGoogleGenAI.createdWithApiKeys.length = 0; // MockGoogleGenAI.streamChunks.length = 0; // MockGoogleGenAI.streamChunks.push({ // candidates: [{ // content: { parts: [{ text: 'Hello from Gemini' }] } // }] // }); // mockHandleAPIKeyUpdate.mockResolvedValue({ apiKey: 'k_test', deleted: false, cancelled: false }); // const storage = createStorageService({ getAPIKey: vi.fn().mockResolvedValue('k_test') }); // const provider = new GeminiNativeBYOKLMProvider(undefined, storage, new TestLogService(), createRequestLogger()); // await provider.updateAPIKey(); // expect(MockGoogleGenAI.createdWithApiKeys).toEqual(['k_test']); // const model: vscode.LanguageModelChatInformation = { // id: 'gemini-2.0-flash', // name: 'Gemini 2.0 Flash', // family: 'Gemini', // version: '1.0.0', // maxInputTokens: 1000, // maxOutputTokens: 1000, // capabilities: { toolCalling: false, imageInput: false } // }; // const messages: vscode.LanguageModelChatMessage[] = [ // new vscode.LanguageModelChatMessage(vscode.LanguageModelChatMessageRole.User, 'hello') // ]; // const tokenSource = new vscode.CancellationTokenSource(); // const progress = new TestProgress(); // await provider.provideLanguageModelChatResponse( // model, // messages, // { requestInitiator: 'test', tools: [], toolMode: vscode.LanguageModelChatToolMode.Auto }, // progress, // tokenSource.token // ); // expect(progress.items.some(p => p instanceof vscode.LanguageModelTextPart && p.value.includes('Hello from Gemini'))).toBe(true); // }); // it.skip('clears the client when API key is deleted via update flow', async () => { // const { GeminiNativeBYOKLMProvider } = await import('../geminiNativeProvider'); // const genai = await import('@google/genai'); // const MockGoogleGenAI = genai.GoogleGenAI as unknown as { createdWithApiKeys: string[]; streamChunks: any[] }; // MockGoogleGenAI.createdWithApiKeys.length = 0; // MockGoogleGenAI.streamChunks.length = 0; // const storage = createStorageService({ getAPIKey: vi.fn().mockResolvedValue(undefined) }); // const provider = new GeminiNativeBYOKLMProvider(undefined, storage, new TestLogService(), createRequestLogger()); // // First set a key // mockHandleAPIKeyUpdate.mockResolvedValueOnce({ apiKey: 'k_initial', deleted: false, cancelled: false }); // await provider.updateAPIKey(); // expect(MockGoogleGenAI.createdWithApiKeys).toEqual(['k_initial']); // // Then delete it // mockHandleAPIKeyUpdate.mockResolvedValueOnce({ apiKey: undefined, deleted: true, cancelled: false }); // await provider.updateAPIKey(); // const model: vscode.LanguageModelChatInformation = { // id: 'gemini-2.0-flash', // name: 'Gemini 2.0 Flash', // family: 'Gemini', // version: '1.0.0', // maxInputTokens: 1000, // maxOutputTokens: 1000, // capabilities: { toolCalling: false, imageInput: false } // }; // const messages: vscode.LanguageModelChatMessage[] = [ // new vscode.LanguageModelChatMessage(vscode.LanguageModelChatMessageRole.User, 'hello') // ]; // const tokenSource = new vscode.CancellationTokenSource(); // const progress = new TestProgress(); // await expect(provider.provideLanguageModelChatResponse( // model, // messages, // { requestInitiator: 'test', tools: [], toolMode: vscode.LanguageModelChatToolMode.Auto }, // progress, // tokenSource.token // )).rejects.toThrow(/No API key configured/i); // }); it.skip('prompts for a new API key when listing models fails with an invalid key', async () => { const { GeminiNativeBYOKLMProvider } = await import('../geminiNativeProvider'); const genai = await import('@google/genai'); const MockGoogleGenAI = genai.GoogleGenAI as unknown as { listModelsResult: AsyncIterable }; // Simulate the models.list() call throwing an invalid API key error when iterated MockGoogleGenAI.listModelsResult = (async function* () { throw new Error('ApiError: {"error":{"message":"API key not valid. Please pass a valid API key.","details":[{"reason":"API_KEY_INVALID"}]}}'); })(); const storage = createStorageService({ getAPIKey: vi.fn().mockResolvedValue('bad_key'), }); mockHandleAPIKeyUpdate.mockResolvedValue({ apiKey: undefined, deleted: false, cancelled: true }); const provider = new GeminiNativeBYOKLMProvider(undefined, storage, new TestLogService(), createRequestLogger(), new NullTelemetryService(), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '1.0.0', sessionId: 'test' }))); const tokenSource = new vscode.CancellationTokenSource(); const models = await provider.provideLanguageModelChatInformation({ silent: false }, tokenSource.token); // When the key is invalid, we should re-prompt for a new one // and handle the failure gracefully by returning an empty list. expect(models).toEqual([]); expect(mockHandleAPIKeyUpdate).toHaveBeenCalled(); }); it.skip('retries listing models after re-prompting with a valid API key', async () => { const { GeminiNativeBYOKLMProvider } = await import('../geminiNativeProvider'); const genai = await import('@google/genai'); const MockGoogleGenAI = genai.GoogleGenAI as unknown as { listModelsResult: AsyncIterable }; let iterationCount = 0; let hasThrown = false; const modelId = 'test-model'; MockGoogleGenAI.listModelsResult = { async *[Symbol.asyncIterator]() { iterationCount++; if (!hasThrown) { hasThrown = true; throw new Error('ApiError: {"error":{"message":"API key not valid. Please pass a valid API key.","details":[{"reason":"API_KEY_INVALID"}]}}'); } yield { name: modelId }; } }; const storage = createStorageService({ getAPIKey: vi.fn().mockResolvedValue('bad_key'), }); mockHandleAPIKeyUpdate.mockResolvedValue({ apiKey: 'k_new', deleted: false, cancelled: false }); const knownModels = { [modelId]: { name: 'Test Model', maxInputTokens: 1000, maxOutputTokens: 1000, toolCalling: false, vision: false } }; const provider = new GeminiNativeBYOKLMProvider(knownModels, storage, new TestLogService(), createRequestLogger(), new NullTelemetryService(), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '1.0.0', sessionId: 'test' }))); const tokenSource = new vscode.CancellationTokenSource(); const models = await provider.provideLanguageModelChatInformation({ silent: false }, tokenSource.token); // First attempt should fail with invalid key, then after re-prompting // we should retry listing models and succeed with the new key. expect(models.map(m => m.id)).toEqual([modelId]); expect(iterationCount).toBe(2); expect(mockHandleAPIKeyUpdate).toHaveBeenCalled(); }); }); ================================================ FILE: src/extension/byok/vscode-node/test/ollamaProvider.spec.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { describe, expect, it, vi } from 'vitest'; import * as vscode from 'vscode'; import { OllamaLMProvider } from '../ollamaProvider'; describe('OllamaLMProvider', () => { it('returns successful models when one /api/show lookup fails', async () => { const ollamaBaseUrl = 'http://localhost:11434'; const tagsModels = [{ model: 'good-model-a' }, { model: 'bad-model' }, { model: 'good-model-b' }]; const showCalls: string[] = []; const fetch = vi.fn(async (url: string, options: { body?: string }) => { if (url === `${ollamaBaseUrl}/api/version`) { return { json: async () => ({ version: '0.6.4' }) }; } if (url === `${ollamaBaseUrl}/api/tags`) { return { json: async () => ({ models: tagsModels }) }; } if (url === `${ollamaBaseUrl}/api/show`) { const modelId = JSON.parse(options.body ?? '{}').model as string; showCalls.push(modelId); if (modelId === 'bad-model') { throw new Error('simulated /api/show failure'); } return { json: async () => ({ template: '', capabilities: [], details: { family: 'llama' }, remote_model: modelId, model_info: { 'general.basename': modelId, 'general.architecture': 'llama', 'llama.context_length': 8192, }, }) }; } throw new Error(`Unexpected URL in test: ${url}`); }); const logService = { _serviceBrand: undefined, trace: vi.fn(), debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), show: vi.fn(), createSubLogger: vi.fn(), withExtraTarget: vi.fn(), }; logService.createSubLogger.mockReturnValue(logService); logService.withExtraTarget.mockReturnValue(logService); const provider = new OllamaLMProvider( { getAPIKey: vi.fn().mockResolvedValue(undefined), storeAPIKey: vi.fn().mockResolvedValue(undefined), deleteAPIKey: vi.fn().mockResolvedValue(undefined), getStoredModelConfigs: vi.fn().mockResolvedValue({}), saveModelConfig: vi.fn().mockResolvedValue(undefined), removeModelConfig: vi.fn().mockResolvedValue(undefined), } as any, { fetch } as any, { isConfigured: vi.fn().mockReturnValue(false), getConfig: vi.fn(), setConfig: vi.fn(), } as any, logService as any, { createInstance: vi.fn().mockReturnValue({}), } as any, {} as any ); const tokenSource = new vscode.CancellationTokenSource(); const models = await provider.provideLanguageModelChatInformation( { silent: false, configuration: { url: ollamaBaseUrl }, }, tokenSource.token ); expect(showCalls).toEqual(['good-model-a', 'bad-model', 'good-model-b']); expect(models.map(model => model.id)).toEqual(['good-model-a', 'good-model-b']); }); }); ================================================ FILE: src/extension/byok/vscode-node/xAIProvider.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { ILogService } from '../../../platform/log/common/logService'; import { IFetcherService } from '../../../platform/networking/common/fetcherService'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { BYOKKnownModels, BYOKModelCapabilities } from '../common/byokProvider'; import { AbstractOpenAICompatibleLMProvider } from './abstractLanguageModelChatProvider'; import { IBYOKStorageService } from './byokStorageService'; // https://docs.x.ai/docs/api-reference#list-language-models interface XAIModelData { id: string; fingerprint: string; created: number; object: string; owned_by: string; input_modalities: string[]; output_modalities: string[]; prompt_text_token_price: number; cached_prompt_text_token_price: number; prompt_image_token_price: number; completion_text_token_price: number; search_price?: number; version: string; aliases: string[]; } export class XAIBYOKLMProvider extends AbstractOpenAICompatibleLMProvider { public static readonly providerName = 'xAI'; constructor( knownModels: BYOKKnownModels, byokStorageService: IBYOKStorageService, @IFetcherService fetcherService: IFetcherService, @ILogService logService: ILogService, @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @IExperimentationService expService: IExperimentationService ) { super( XAIBYOKLMProvider.providerName.toLowerCase(), XAIBYOKLMProvider.providerName, knownModels, byokStorageService, fetcherService, logService, instantiationService, configurationService, expService ); } protected getModelsBaseUrl(): string | undefined { return 'https://api.x.ai/v1'; } protected override getModelsDiscoveryUrl(modelsBaseUrl: string): string { return `${modelsBaseUrl}/language-models`; } protected override resolveModelCapabilities(modelData: unknown): BYOKModelCapabilities | undefined { const xaiModelData = modelData as XAIModelData; // Add new model with reasonable defaults let maxInputTokens; let maxOutputTokens; // Coding models and Grok 4+ models have larger context windows const parsedVersion = this.parseXAIModelVersion(xaiModelData.id) ?? 0; if (xaiModelData.id.startsWith('grok-code') || parsedVersion >= 4) { maxInputTokens = 120000; maxOutputTokens = 120000; } else { maxInputTokens = 80000; maxOutputTokens = 30000; } return { name: this.humanizeXAIModelId(xaiModelData.id), toolCalling: true, vision: xaiModelData.input_modalities.includes('image'), maxInputTokens, maxOutputTokens, }; } private parseXAIModelVersion(modelId: string): number | undefined { const match = modelId.match(/^grok-(\d+)/); return match ? parseInt(match[1], 10) : undefined; } private humanizeXAIModelId(modelId: string): string { const parts = modelId.split('-').filter(p => p.length > 0); return parts.map(p => { if (/^\d+$/.test(p)) { return p; // keep pure numbers as-is } return p.charAt(0).toUpperCase() + p.slice(1); }).join(' '); } } ================================================ FILE: src/extension/chat/test/node/chatHookService.spec.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { beforeEach, describe, expect, it } from 'vitest'; import type { ChatHookCommand, ChatHookResult, ChatHookResultKind, ChatRequestHooks, Uri } from 'vscode'; import { IPostToolUseHookResult, IPreToolUseHookResult } from '../../../../platform/chat/common/chatHookService'; import { HookCommandResultKind, IHookCommandResult } from '../../../../platform/chat/common/hookExecutor'; import { IToolValidationResult } from '../../../tools/common/toolsService'; function cmd(command: string, cwd?: Uri): ChatHookCommand { return { command, cwd } as ChatHookCommand; } /** * A testable version of ChatHookService.executeHook logic, * reimplemented here to stay within the layering constraints. * This mirrors the real implementation's result conversion and iteration logic. */ class TestableExecuteHookService { public executorCalls: Array<{ hookCommand: ChatHookCommand; input: unknown }> = []; public executorHandler: (hookCommand: ChatHookCommand, input: unknown) => IHookCommandResult = () => ({ kind: HookCommandResultKind.Success, result: '' }); public transcriptPath: Uri | undefined; public flushedSessionIds: string[] = []; async executeHook(hookType: string, hooks: ChatRequestHooks | undefined, input: unknown, sessionId?: string): Promise { if (!hooks) { return []; } const hookCommands = hooks[hookType]; if (!hookCommands || hookCommands.length === 0) { return []; } if (sessionId) { this.flushedSessionIds.push(sessionId); } const commonInput = { timestamp: new Date().toISOString(), hook_event_name: hookType, ...(sessionId ? { session_id: sessionId } : undefined), ...(this.transcriptPath ? { transcript_path: this.transcriptPath } : undefined), }; const fullInput = (typeof input === 'object' && input !== null) ? { ...commonInput, ...input } : commonInput; const results: ChatHookResult[] = []; for (const hookCommand of hookCommands) { try { const commandInput = hookCommand.cwd ? { ...fullInput, cwd: hookCommand.cwd } : fullInput; this.executorCalls.push({ hookCommand, input: commandInput }); const commandResult = this.executorHandler(hookCommand, commandInput); const result = this._toHookResult(hookType, commandResult); results.push(result); if (result.stopReason !== undefined) { break; } } catch (err) { results.push({ resultKind: 'warning', output: undefined, warningMessage: err instanceof Error ? err.message : String(err), }); } } return results; } private _toHookResult(hookType: string, commandResult: IHookCommandResult): ChatHookResult { switch (commandResult.kind) { case HookCommandResultKind.Error: { const message = typeof commandResult.result === 'string' ? commandResult.result : JSON.stringify(commandResult.result); return { resultKind: 'error', output: message }; } case HookCommandResultKind.NonBlockingError: { const errorMessage = typeof commandResult.result === 'string' ? commandResult.result : JSON.stringify(commandResult.result); return { resultKind: 'warning', output: undefined, warningMessage: errorMessage }; } case HookCommandResultKind.Success: { if (typeof commandResult.result !== 'object') { return { resultKind: 'success', output: commandResult.result }; } const resultObj = commandResult.result as Record; const stopReason = typeof resultObj['stopReason'] === 'string' ? resultObj['stopReason'] : undefined; const continueFlag = resultObj['continue']; const systemMessage = typeof resultObj['systemMessage'] === 'string' ? resultObj['systemMessage'] : undefined; let effectiveStopReason = stopReason; if (continueFlag === false && !effectiveStopReason) { effectiveStopReason = ''; } // Check hookEventName at top level — if present and mismatched, skip this result const topLevelHookEventName = resultObj['hookEventName']; if (typeof topLevelHookEventName === 'string' && topLevelHookEventName !== hookType) { return { resultKind: 'success', output: undefined }; } // Check hookEventName inside hookSpecificOutput — if mismatched, strip hookSpecificOutput but keep the rest let stripHookSpecificOutput = false; const hookSpecificOutput = resultObj['hookSpecificOutput']; if (typeof hookSpecificOutput === 'object' && hookSpecificOutput !== null) { const nestedHookEventName = (hookSpecificOutput as Record)['hookEventName']; if (typeof nestedHookEventName === 'string' && nestedHookEventName !== hookType) { stripHookSpecificOutput = true; } } const commonFields = new Set(['continue', 'stopReason', 'systemMessage']); if (stripHookSpecificOutput) { commonFields.add('hookSpecificOutput'); } const hookOutput: Record = {}; for (const [key, value] of Object.entries(resultObj)) { if (value !== undefined && !commonFields.has(key)) { hookOutput[key] = value; } } return { resultKind: 'success', stopReason: effectiveStopReason, warningMessage: systemMessage, output: Object.keys(hookOutput).length > 0 ? hookOutput : undefined, }; } default: return { resultKind: 'warning', warningMessage: `Unexpected hook command result kind: ${(commandResult as IHookCommandResult).kind}`, output: undefined }; } } } describe('ChatHookService.executeHook', () => { let service: TestableExecuteHookService; beforeEach(() => { service = new TestableExecuteHookService(); }); it('returns empty array when hooks is undefined', async () => { const results = await service.executeHook('Stop', undefined, {}); expect(results).toEqual([]); }); it('returns empty array when no commands for hook type', async () => { const results = await service.executeHook('Stop', { PreToolUse: [cmd('echo test')] }, {}); expect(results).toEqual([]); }); it('executes hook and returns success result', async () => { service.executorHandler = () => ({ kind: HookCommandResultKind.Success, result: { decision: 'block', reason: 'test' } }); const results = await service.executeHook('Stop', { Stop: [cmd('echo test')] }, {}); expect(results).toHaveLength(1); expect(results[0].resultKind).toBe('success'); expect(results[0].output).toEqual({ decision: 'block', reason: 'test' }); }); it('converts exit code 2 to error result with message in output', async () => { service.executorHandler = () => ({ kind: HookCommandResultKind.Error, result: 'fatal error' }); const results = await service.executeHook('Stop', { Stop: [cmd('fail')] }, {}); expect(results).toHaveLength(1); expect(results[0].resultKind).toBe('error'); expect(results[0].output).toBe('fatal error'); expect(results[0].stopReason).toBeUndefined(); }); it('does not stop processing on error results (callers decide)', async () => { let callCount = 0; service.executorHandler = () => { callCount++; if (callCount === 1) { return { kind: HookCommandResultKind.Error, result: 'error from first' }; } return { kind: HookCommandResultKind.Success, result: 'second ok' }; }; const results = await service.executeHook('Stop', { Stop: [cmd('first'), cmd('second')] }, {}); expect(results).toHaveLength(2); expect(callCount).toBe(2); expect(results[0].resultKind).toBe('error'); expect(results[1].resultKind).toBe('success'); }); it('converts non-blocking error to warning', async () => { service.executorHandler = () => ({ kind: HookCommandResultKind.NonBlockingError, result: 'warning msg' }); const results = await service.executeHook('Stop', { Stop: [cmd('warn')] }, {}); expect(results).toHaveLength(1); expect(results[0].resultKind).toBe('warning'); expect(results[0].warningMessage).toBe('warning msg'); expect(results[0].stopReason).toBeUndefined(); }); it('stops processing after first hook with stopReason', async () => { let callCount = 0; service.executorHandler = () => { callCount++; if (callCount === 1) { return { kind: HookCommandResultKind.Success, result: { stopReason: 'stop here' } }; } return { kind: HookCommandResultKind.Success, result: 'second' }; }; const results = await service.executeHook('Stop', { Stop: [cmd('first'), cmd('second')] }, {}); expect(results).toHaveLength(1); expect(callCount).toBe(1); expect(results[0].stopReason).toBe('stop here'); }); it('stops processing on empty string stopReason (continue: false)', async () => { let callCount = 0; service.executorHandler = () => { callCount++; return { kind: HookCommandResultKind.Success, result: { continue: false } }; }; const results = await service.executeHook('Stop', { Stop: [cmd('first'), cmd('second')] }, {}); expect(results).toHaveLength(1); expect(callCount).toBe(1); expect(results[0].stopReason).toBe(''); }); it('catches executor errors and returns warning', async () => { service.executorHandler = () => { throw new Error('spawn failed'); }; const results = await service.executeHook('Stop', { Stop: [cmd('fail')] }, {}); expect(results).toHaveLength(1); expect(results[0].resultKind).toBe('warning'); expect(results[0].warningMessage).toBe('spawn failed'); }); it('includes sessionId in common input', async () => { service.executorHandler = () => ({ kind: HookCommandResultKind.Success, result: '' }); await service.executeHook('Stop', { Stop: [cmd('test')] }, {}, 'session-123'); expect(service.executorCalls[0].input).toMatchObject({ session_id: 'session-123', hook_event_name: 'Stop' }); }); it('includes cwd from hook command in input', async () => { const cwdUri = { scheme: 'file', path: '/my/project' } as Uri; service.executorHandler = () => ({ kind: HookCommandResultKind.Success, result: '' }); await service.executeHook('Stop', { Stop: [cmd('test', cwdUri)] }, {}); expect(service.executorCalls[0].input).toMatchObject({ cwd: cwdUri }); }); it('merges caller input with common input', async () => { service.executorHandler = () => ({ kind: HookCommandResultKind.Success, result: '' }); await service.executeHook('PreToolUse', { PreToolUse: [cmd('test')] }, { tool_name: 'myTool', tool_input: { x: 1 } }); const input = service.executorCalls[0].input as Record; expect(input['tool_name']).toBe('myTool'); expect(input['tool_input']).toEqual({ x: 1 }); expect(input['hook_event_name']).toBe('PreToolUse'); expect(typeof input['timestamp']).toBe('string'); }); it('includes transcript_path when configured', async () => { const transcriptUri = { scheme: 'file', path: '/tmp/transcript.jsonl' } as Uri; service.transcriptPath = transcriptUri; service.executorHandler = () => ({ kind: HookCommandResultKind.Success, result: '' }); await service.executeHook('Stop', { Stop: [cmd('test')] }, {}, 'session-1'); expect(service.flushedSessionIds).toContain('session-1'); expect(service.executorCalls[0].input).toMatchObject({ transcript_path: transcriptUri }); }); it('extracts systemMessage as warningMessage', async () => { service.executorHandler = () => ({ kind: HookCommandResultKind.Success, result: { systemMessage: 'be careful' }, }); const results = await service.executeHook('Stop', { Stop: [cmd('test')] }, {}); expect(results[0].warningMessage).toBe('be careful'); }); it('separates common fields from hook-specific output', async () => { service.executorHandler = () => ({ kind: HookCommandResultKind.Success, result: { continue: true, systemMessage: 'msg', decision: 'block', reason: 'test' }, }); const results = await service.executeHook('Stop', { Stop: [cmd('test')] }, {}); expect(results[0].output).toEqual({ decision: 'block', reason: 'test' }); expect(results[0].warningMessage).toBe('msg'); expect(results[0].stopReason).toBeUndefined(); }); it('executes multiple hooks in sequence', async () => { const commands: string[] = []; service.executorHandler = (hookCmd) => { commands.push(hookCmd.command); return { kind: HookCommandResultKind.Success, result: '' }; }; const results = await service.executeHook('Stop', { Stop: [cmd('a'), cmd('b'), cmd('c')] }, {}); expect(results).toHaveLength(3); expect(commands).toEqual(['a', 'b', 'c']); }); it('filters out results with mismatched top-level hookEventName', async () => { service.executorHandler = () => ({ kind: HookCommandResultKind.Success, result: { hookEventName: 'PreToolUse', decision: 'block', reason: 'wrong event' }, }); const results = await service.executeHook('Stop', { Stop: [cmd('test')] }, {}); expect(results).toHaveLength(1); expect(results[0].resultKind).toBe('success'); expect(results[0].output).toBeUndefined(); }); it('strips hookSpecificOutput with mismatched nested hookEventName but keeps other fields', async () => { service.executorHandler = () => ({ kind: HookCommandResultKind.Success, result: { hookSpecificOutput: { hookEventName: 'PostToolUse', permissionDecision: 'deny' }, decision: 'block', reason: 'kept' }, }); const results = await service.executeHook('PreToolUse', { PreToolUse: [cmd('test')] }, {}); expect(results).toHaveLength(1); expect(results[0].resultKind).toBe('success'); expect(results[0].output).toEqual({ decision: 'block', reason: 'kept' }); }); it('discards entire output when hookSpecificOutput is the only non-common field and hookEventName mismatches', async () => { service.executorHandler = () => ({ kind: HookCommandResultKind.Success, result: { hookSpecificOutput: { hookEventName: 'PostToolUse', permissionDecision: 'deny' } }, }); const results = await service.executeHook('PreToolUse', { PreToolUse: [cmd('test')] }, {}); expect(results).toHaveLength(1); expect(results[0].resultKind).toBe('success'); expect(results[0].output).toBeUndefined(); }); it('allows results with matching hookEventName', async () => { service.executorHandler = () => ({ kind: HookCommandResultKind.Success, result: { hookEventName: 'Stop', decision: 'block', reason: 'correct event' }, }); const results = await service.executeHook('Stop', { Stop: [cmd('test')] }, {}); expect(results).toHaveLength(1); expect(results[0].output).toEqual({ hookEventName: 'Stop', decision: 'block', reason: 'correct event' }); }); it('allows results without hookEventName', async () => { service.executorHandler = () => ({ kind: HookCommandResultKind.Success, result: { decision: 'block', reason: 'no event name' }, }); const results = await service.executeHook('Stop', { Stop: [cmd('test')] }, {}); expect(results).toHaveLength(1); expect(results[0].output).toEqual({ decision: 'block', reason: 'no event name' }); }); }); /** * Minimal mock of ChatHookService that exposes executePreToolUseHook * without requiring the real vscode API. * * We replicate the collapsing logic from ChatHookService.executePreToolUseHook * by subclassing and overriding executeHook to return configurable results. */ interface IPreToolUseHookSpecificOutput { hookEventName?: string; permissionDecision?: 'allow' | 'deny' | 'ask'; permissionDecisionReason?: string; updatedInput?: object; additionalContext?: string; } const permissionPriority: Record = { 'deny': 2, 'ask': 1, 'allow': 0 }; /** * A testable version of the executePreToolUseHook collapsing logic, * decoupled from the vscode API. Takes raw ChatHookResult[] and returns * the collapsed IPreToolUseHookResult. */ function collapsePreToolUseHookResults(results: ChatHookResult[]): IPreToolUseHookResult | undefined { if (results.length === 0) { return undefined; } let mostRestrictiveDecision: 'allow' | 'deny' | 'ask' | undefined; let winningReason: string | undefined; let lastUpdatedInput: object | undefined; const allAdditionalContext: string[] = []; for (const result of results) { // Exit code 2 (error) means deny the tool if (result.resultKind === 'error') { const reason = typeof result.output === 'string' ? result.output : undefined; mostRestrictiveDecision = 'deny'; winningReason = reason ?? winningReason; break; } if (result.resultKind !== 'success' || typeof result.output !== 'object' || result.output === null) { continue; } const output = result.output as { hookSpecificOutput?: IPreToolUseHookSpecificOutput }; const hookSpecificOutput = output.hookSpecificOutput; if (!hookSpecificOutput) { continue; } if (hookSpecificOutput.hookEventName !== undefined && hookSpecificOutput.hookEventName !== 'PreToolUse') { continue; } if (hookSpecificOutput.additionalContext) { allAdditionalContext.push(hookSpecificOutput.additionalContext); } if (hookSpecificOutput.updatedInput) { lastUpdatedInput = hookSpecificOutput.updatedInput; } const decision = hookSpecificOutput.permissionDecision; if (decision && (mostRestrictiveDecision === undefined || (permissionPriority[decision] ?? 0) > (permissionPriority[mostRestrictiveDecision] ?? 0))) { mostRestrictiveDecision = decision; winningReason = hookSpecificOutput.permissionDecisionReason; } } if (!mostRestrictiveDecision && !lastUpdatedInput && allAdditionalContext.length === 0) { return undefined; } return { permissionDecision: mostRestrictiveDecision, permissionDecisionReason: winningReason, updatedInput: lastUpdatedInput, additionalContext: allAdditionalContext.length > 0 ? allAdditionalContext : undefined, }; } function hookResult(output: unknown, kind: ChatHookResultKind = 'success'): ChatHookResult { return { resultKind: kind, output } as ChatHookResult; } /** * A testable ChatHookService that stubs executeHook to return configurable results, * so we can test executePreToolUseHook's collapsing logic without the real vscode API. */ class TestableChatHookService { public hookResults: ChatHookResult[] = []; public validateToolInputFn: ((name: string, input: string) => IToolValidationResult) | undefined; async executeHook(): Promise { return this.hookResults; } async executePreToolUseHook( toolName: string, toolInput: unknown, toolCallId: string, toolInvocationToken: unknown, sessionId?: string, ): Promise { const results = await this.executeHook(); const collapsed = collapsePreToolUseHookResults(results); if (!collapsed) { return undefined; } // Validate updatedInput against the tool's input schema, mirroring the real ChatHookService if (collapsed.updatedInput && this.validateToolInputFn) { const validationResult = this.validateToolInputFn(toolName, JSON.stringify(collapsed.updatedInput)); if ('error' in validationResult) { collapsed.updatedInput = undefined; } } if (!collapsed.permissionDecision && !collapsed.updatedInput && !collapsed.additionalContext?.length) { return undefined; } return collapsed; } } describe('ChatHookService.executePreToolUseHook', () => { let service: TestableChatHookService; beforeEach(() => { service = new TestableChatHookService(); }); it('returns undefined when no hooks return results', async () => { service.hookResults = []; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result).toBeUndefined(); }); it('returns allow when single hook allows', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { permissionDecision: 'allow', permissionDecisionReason: 'Tool is safe' } }), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result).toEqual({ permissionDecision: 'allow', permissionDecisionReason: 'Tool is safe', updatedInput: undefined, additionalContext: undefined, }); }); it('returns deny when single hook denies', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { permissionDecision: 'deny', permissionDecisionReason: 'Blocked' } }), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result).toEqual({ permissionDecision: 'deny', permissionDecisionReason: 'Blocked', updatedInput: undefined, additionalContext: undefined, }); }); it('returns ask when single hook asks', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { permissionDecision: 'ask', permissionDecisionReason: 'Needs review' } }), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result).toEqual({ permissionDecision: 'ask', permissionDecisionReason: 'Needs review', updatedInput: undefined, additionalContext: undefined, }); }); it('deny wins over allow and ask', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { permissionDecision: 'allow', permissionDecisionReason: 'ok' } }), hookResult({ hookSpecificOutput: { permissionDecision: 'ask', permissionDecisionReason: 'maybe' } }), hookResult({ hookSpecificOutput: { permissionDecision: 'deny', permissionDecisionReason: 'nope' } }), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result?.permissionDecision).toBe('deny'); expect(result?.permissionDecisionReason).toBe('nope'); }); it('ask wins over allow', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { permissionDecision: 'allow', permissionDecisionReason: 'ok' } }), hookResult({ hookSpecificOutput: { permissionDecision: 'ask', permissionDecisionReason: 'confirm please' } }), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result?.permissionDecision).toBe('ask'); expect(result?.permissionDecisionReason).toBe('confirm please'); }); it('ignores results with wrong hookEventName', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { hookEventName: 'PostToolUse', permissionDecision: 'deny' } }), hookResult({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' } }), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result?.permissionDecision).toBe('allow'); }); it('accepts results without hookEventName', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { permissionDecision: 'allow' } }), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result?.permissionDecision).toBe('allow'); }); it('returns updatedInput from hook', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { permissionDecision: 'allow', updatedInput: { path: '/safe/path.ts' } } }), ]; const result = await service.executePreToolUseHook('tool', { path: '/original' }, 'call-1', undefined); expect(result?.updatedInput).toEqual({ path: '/safe/path.ts' }); }); it('later hook updatedInput overrides earlier one', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { permissionDecision: 'allow', updatedInput: { value: 'first' } } }), hookResult({ hookSpecificOutput: { permissionDecision: 'allow', updatedInput: { value: 'second' } } }), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result?.updatedInput).toEqual({ value: 'second' }); }); it('returns updatedInput even without permission decision', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { updatedInput: { modified: true } } }), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result?.updatedInput).toEqual({ modified: true }); expect(result?.permissionDecision).toBeUndefined(); }); it('discards updatedInput when schema validation fails', async () => { service.validateToolInputFn = () => ({ error: 'Missing required property "command"' }); service.hookResults = [ hookResult({ hookSpecificOutput: { permissionDecision: 'allow', updatedInput: { invalidField: 'wrong' } } }), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result?.permissionDecision).toBe('allow'); expect(result?.updatedInput).toBeUndefined(); }); it('keeps updatedInput when schema validation passes', async () => { service.validateToolInputFn = (_name, input) => ({ inputObj: JSON.parse(input) }); service.hookResults = [ hookResult({ hookSpecificOutput: { permissionDecision: 'allow', updatedInput: { command: 'safe' } } }), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result?.permissionDecision).toBe('allow'); expect(result?.updatedInput).toEqual({ command: 'safe' }); }); it('returns undefined when only updatedInput is present but fails validation', async () => { service.validateToolInputFn = () => ({ error: 'invalid' }); service.hookResults = [ hookResult({ hookSpecificOutput: { updatedInput: { bad: true } } }), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result).toBeUndefined(); }); it('collects additionalContext from all hooks', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { permissionDecision: 'allow', additionalContext: 'context from hook 1' } }), hookResult({ hookSpecificOutput: { permissionDecision: 'allow', additionalContext: 'context from hook 2' } }), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result?.additionalContext).toEqual(['context from hook 1', 'context from hook 2']); }); it('returns undefined additionalContext when no hooks provide it', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { permissionDecision: 'allow' } }), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result?.additionalContext).toBeUndefined(); }); it('combines updatedInput, additionalContext, and permission decision', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { permissionDecision: 'ask', permissionDecisionReason: 'Modified input needs review', updatedInput: { command: 'echo safe' }, additionalContext: 'audit log enabled' } }), ]; const result = await service.executePreToolUseHook('tool', { command: 'rm -rf /' }, 'call-1', undefined); expect(result).toEqual({ permissionDecision: 'ask', permissionDecisionReason: 'Modified input needs review', updatedInput: { command: 'echo safe' }, additionalContext: ['audit log enabled'], }); }); it('treats error results (exit code 2) as deny', async () => { service.hookResults = [ hookResult('hook blocked this tool', 'error'), hookResult({ hookSpecificOutput: { permissionDecision: 'allow' } }), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result?.permissionDecision).toBe('deny'); expect(result?.permissionDecisionReason).toBe('hook blocked this tool'); }); it('preserves context from prior hooks when error denies', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { permissionDecision: 'allow', additionalContext: 'context from first hook' } }), hookResult('second hook errored', 'error'), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result?.permissionDecision).toBe('deny'); expect(result?.additionalContext).toEqual(['context from first hook']); }); it('skips warning results', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { permissionDecision: 'deny' } }, 'warning'), hookResult({ hookSpecificOutput: { permissionDecision: 'allow' } }), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result?.permissionDecision).toBe('allow'); }); it('skips results with non-object output', async () => { service.hookResults = [ hookResult('string output'), hookResult({ hookSpecificOutput: { permissionDecision: 'allow' } }), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result?.permissionDecision).toBe('allow'); }); it('skips results without hookSpecificOutput', async () => { service.hookResults = [ hookResult({ someOtherField: 'value' }), hookResult({ hookSpecificOutput: { permissionDecision: 'deny', permissionDecisionReason: 'blocked' } }), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result?.permissionDecision).toBe('deny'); }); it('returns undefined when all results are warnings', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { permissionDecision: 'deny' } }, 'warning'), hookResult({ hookSpecificOutput: { permissionDecision: 'allow' } }, 'warning'), ]; const result = await service.executePreToolUseHook('tool', {}, 'call-1', undefined); expect(result).toBeUndefined(); }); }); interface IPostToolUseHookSpecificOutput { hookEventName?: string; additionalContext?: string; } function collapsePostToolUseHookResults(results: ChatHookResult[]): IPostToolUseHookResult | undefined { if (results.length === 0) { return undefined; } let hasBlock = false; let blockReason: string | undefined; const allAdditionalContext: string[] = []; for (const result of results) { // Exit code 2 (error) means block the tool result if (result.resultKind === 'error') { const reason = typeof result.output === 'string' ? result.output : undefined; if (!hasBlock) { hasBlock = true; blockReason = reason; } break; } if (result.resultKind !== 'success' || typeof result.output !== 'object' || result.output === null) { continue; } const output = result.output as { decision?: string; reason?: string; hookSpecificOutput?: IPostToolUseHookSpecificOutput; }; if (output.hookSpecificOutput?.hookEventName !== undefined && output.hookSpecificOutput.hookEventName !== 'PostToolUse') { continue; } if (output.hookSpecificOutput?.additionalContext) { allAdditionalContext.push(output.hookSpecificOutput.additionalContext); } if (output.decision === 'block' && !hasBlock) { hasBlock = true; blockReason = output.reason; } } if (!hasBlock && allAdditionalContext.length === 0) { return undefined; } return { decision: hasBlock ? 'block' : undefined, reason: blockReason, additionalContext: allAdditionalContext.length > 0 ? allAdditionalContext : undefined, }; } class TestablePostToolUseChatHookService { public hookResults: ChatHookResult[] = []; async executeHook(): Promise { return this.hookResults; } async executePostToolUseHook( toolName: string, toolInput: unknown, toolResponseText: string, toolCallId: string, toolInvocationToken: unknown, sessionId?: string, ): Promise { const results = await this.executeHook(); return collapsePostToolUseHookResults(results); } } describe('ChatHookService.executePostToolUseHook', () => { let service: TestablePostToolUseChatHookService; beforeEach(() => { service = new TestablePostToolUseChatHookService(); }); it('returns undefined when no hooks return results', async () => { service.hookResults = []; const result = await service.executePostToolUseHook('tool', {}, 'output', 'call-1', undefined); expect(result).toBeUndefined(); }); it('returns block decision when hook blocks', async () => { service.hookResults = [ hookResult({ decision: 'block', reason: 'Lint errors found' }), ]; const result = await service.executePostToolUseHook('tool', {}, 'output', 'call-1', undefined); expect(result).toEqual({ decision: 'block', reason: 'Lint errors found', additionalContext: undefined, }); }); it('returns additionalContext from hookSpecificOutput', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { additionalContext: 'Tests still pass' } }), ]; const result = await service.executePostToolUseHook('tool', {}, 'output', 'call-1', undefined); expect(result).toEqual({ decision: undefined, reason: undefined, additionalContext: ['Tests still pass'], }); }); it('collects additionalContext from all hooks', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { additionalContext: 'context from hook 1' } }), hookResult({ hookSpecificOutput: { additionalContext: 'context from hook 2' } }), ]; const result = await service.executePostToolUseHook('tool', {}, 'output', 'call-1', undefined); expect(result?.additionalContext).toEqual(['context from hook 1', 'context from hook 2']); }); it('first block decision wins', async () => { service.hookResults = [ hookResult({ decision: 'block', reason: 'First block' }), hookResult({ decision: 'block', reason: 'Second block' }), ]; const result = await service.executePostToolUseHook('tool', {}, 'output', 'call-1', undefined); expect(result?.decision).toBe('block'); expect(result?.reason).toBe('First block'); }); it('block decision with additionalContext from different hooks', async () => { service.hookResults = [ hookResult({ decision: 'block', reason: 'Tests failed' }), hookResult({ hookSpecificOutput: { additionalContext: 'Extra context from linter' } }), ]; const result = await service.executePostToolUseHook('tool', {}, 'output', 'call-1', undefined); expect(result).toEqual({ decision: 'block', reason: 'Tests failed', additionalContext: ['Extra context from linter'], }); }); it('ignores results with wrong hookEventName', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Should be ignored' } }), hookResult({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: 'Correct context' } }), ]; const result = await service.executePostToolUseHook('tool', {}, 'output', 'call-1', undefined); expect(result?.additionalContext).toEqual(['Correct context']); }); it('accepts results without hookEventName', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { additionalContext: 'No event name' } }), ]; const result = await service.executePostToolUseHook('tool', {}, 'output', 'call-1', undefined); expect(result?.additionalContext).toEqual(['No event name']); }); it('treats error results (exit code 2) as block', async () => { service.hookResults = [ hookResult('hook errored', 'error'), hookResult({ hookSpecificOutput: { additionalContext: 'Valid context' } }), ]; const result = await service.executePostToolUseHook('tool', {}, 'output', 'call-1', undefined); expect(result?.decision).toBe('block'); expect(result?.reason).toBe('hook errored'); }); it('preserves context from prior hooks when error blocks', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: { additionalContext: 'context from first' } }), hookResult('second errored', 'error'), ]; const result = await service.executePostToolUseHook('tool', {}, 'output', 'call-1', undefined); expect(result?.decision).toBe('block'); expect(result?.additionalContext).toEqual(['context from first']); }); it('skips warning results', async () => { service.hookResults = [ hookResult({ decision: 'block', reason: 'Should be ignored' }, 'warning'), hookResult({ hookSpecificOutput: { additionalContext: 'Valid context' } }), ]; const result = await service.executePostToolUseHook('tool', {}, 'output', 'call-1', undefined); expect(result?.decision).toBeUndefined(); expect(result?.additionalContext).toEqual(['Valid context']); }); it('skips results with non-object output', async () => { service.hookResults = [ hookResult('string output'), hookResult({ decision: 'block', reason: 'Valid block' }), ]; const result = await service.executePostToolUseHook('tool', {}, 'output', 'call-1', undefined); expect(result?.decision).toBe('block'); }); it('returns undefined when all results are warnings', async () => { service.hookResults = [ hookResult({ decision: 'block' }, 'warning'), hookResult({ hookSpecificOutput: { additionalContext: 'ctx' } }, 'warning'), ]; const result = await service.executePostToolUseHook('tool', {}, 'output', 'call-1', undefined); expect(result).toBeUndefined(); }); it('returns undefined when no hook provides block or additionalContext', async () => { service.hookResults = [ hookResult({ hookSpecificOutput: {} }), ]; const result = await service.executePostToolUseHook('tool', {}, 'output', 'call-1', undefined); expect(result).toBeUndefined(); }); }); ================================================ FILE: src/extension/chat/vscode-node/chatDebugFileLoggerService.ts ================================================ /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; import * as vscode from 'vscode'; import { IChatDebugFileLoggerService, sessionResourceToId } from '../../../platform/chat/common/chatDebugFileLoggerService'; import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; import { createDirectoryIfNotExists, IFileSystemService } from '../../../platform/filesystem/common/fileSystemService'; import { ILogService } from '../../../platform/log/common/logService'; import { CopilotChatAttr, GenAiAttr, GenAiOperationName } from '../../../platform/otel/common/index'; import { ICompletedSpanData, IOTelService, ISpanEventData, SpanStatusCode } from '../../../platform/otel/common/otelService'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; import { Disposable } from '../../../util/vs/base/common/lifecycle'; import { extUriBiasedIgnorePathCase } from '../../../util/vs/base/common/resources'; import { URI } from '../../../util/vs/base/common/uri'; import { IExtensionContribution } from '../../common/contributions'; const DEBUG_LOGS_DIR_NAME = 'debug-logs'; const MAX_RETAINED_LOGS = 50; const DEFAULT_FLUSH_INTERVAL_MS = 4_000; const MIN_FLUSH_INTERVAL_MS = 2_000; const MAX_ATTR_VALUE_LENGTH = 5_000; const MAX_PENDING_CORE_EVENTS = 100; const MAX_SESSION_LOG_BYTES = 100 * 1024 * 1024; // 100MB const TRUNCATION_RETAIN_BYTES = 60 * 1024 * 1024; // 60 MB const MAX_SPAN_SESSION_INDEX = 10_000; interface IActiveLogSession { readonly uri: URI; /** The directory containing this session's log files */ readonly sessionDir: URI; readonly buffer: string[]; flushPromise: Promise; dirEnsured: boolean; bytesWritten: number; /** Parent session ID if this is a child session (e.g., title, categorization) */ readonly parentSessionId?: string; /** Label for child sessions (e.g., 'title', 'categorization') */ readonly label?: string; /** Whether this session has received its own OTel spans (vs being auto-created as a parent ref) */ hasOwnSpans: boolean; } /** * A single JSONL debug log entry. */ interface IDebugLogEntry { /** Epoch ms timestamp */ readonly ts: number; /** Duration in ms (0 for instant events) */ readonly dur: number; /** Chat session ID */ readonly sid: string; /** Event type */ readonly type: 'tool_call' | 'llm_request' | 'user_message' | 'agent_response' | 'subagent' | 'discovery' | 'error' | 'generic' | 'child_session_ref' | 'hook'; /** Descriptive name */ readonly name: string; /** Span or event ID */ readonly spanId: string; /** Parent span ID for hierarchy */ readonly parentSpanId?: string; /** Status */ readonly status: 'ok' | 'error'; /** Type-specific attributes */ readonly attrs: Record; } export class ChatDebugFileLoggerService extends Disposable implements IChatDebugFileLoggerService { declare readonly _serviceBrand: undefined; public readonly id = 'chatDebugFileLogger'; private readonly _activeSessions = new Map(); /** Maps child session ID → { parentSessionId, label } for child session routing */ private readonly _childSessionMap = new Map(); /** Maps spanId → resolved session ID for parent-span inheritance */ private readonly _spanSessionIndex = new Map(); private readonly _pendingCoreEvents: IDebugLogEntry[] = []; private _debugLogsDirUri: URI | undefined; private _autoFlushTimer: ReturnType | undefined; private _autoFlushIntervalMs: number; private _totalBytesWritten = 0; private _totalSessionCount = 0; constructor( @IOTelService private readonly _otelService: IOTelService, @IFileSystemService private readonly _fileSystemService: IFileSystemService, @IVSCodeExtensionContext private readonly _extensionContext: IVSCodeExtensionContext, @ILogService private readonly _logService: ILogService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IExperimentationService private readonly _experimentationService: IExperimentationService, @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { super(); const enabled = this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.ChatDebugFileLogging, this._experimentationService); if (!enabled) { /* __GDPR__ "chatDebugFileLogger.disabled" : { "owner": "vijayupadya", "comment": "Chat debug file logging is disabled via experiment or config" } */ this._telemetryService.sendMSFTTelemetryEvent('chatDebugFileLogger.disabled'); this._autoFlushIntervalMs = DEFAULT_FLUSH_INTERVAL_MS; return; } this._autoFlushIntervalMs = Math.max(MIN_FLUSH_INTERVAL_MS, this._configurationService.getConfig(ConfigKey.Advanced.ChatDebugFileLoggingFlushInterval) ?? DEFAULT_FLUSH_INTERVAL_MS); // React to flush interval changes at runtime this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(ConfigKey.Advanced.ChatDebugFileLoggingFlushInterval.fullyQualifiedId)) { this._autoFlushIntervalMs = Math.max(MIN_FLUSH_INTERVAL_MS, this._configurationService.getConfig(ConfigKey.Advanced.ChatDebugFileLoggingFlushInterval) ?? DEFAULT_FLUSH_INTERVAL_MS); this._restartFlushTimer(); } })); // Subscribe to OTel span completions this._register(this._otelService.onDidCompleteSpan(span => { this._onSpanCompleted(span); })); // Subscribe to OTel span events (real-time user messages) this._register(this._otelService.onDidEmitSpanEvent(event => { this._onSpanEvent(event); })); // Subscribe to core debug events (discovery, skill loading, etc.) if (typeof vscode.chat?.onDidReceiveChatDebugEvent === 'function') { this._register(vscode.chat.onDidReceiveChatDebugEvent(event => { this._onCoreDebugEvent(event); })); } } override dispose(): void { if (this._autoFlushTimer) { clearInterval(this._autoFlushTimer); this._autoFlushTimer = undefined; } // Accumulate any remaining active session bytes before emitting telemetry for (const session of this._activeSessions.values()) { this._totalBytesWritten += session.bytesWritten; } /* __GDPR__ "chatDebugFileLogger.end" : { "owner": "vijayupadya", "comment": "Chat debug file logger is being disposed", "totalBytesWritten": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "Total bytes written across all sessions" }, "sessionCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "Total number of sessions logged" } } */ this._telemetryService.sendMSFTTelemetryEvent('chatDebugFileLogger.end', undefined, { totalBytesWritten: this._totalBytesWritten, sessionCount: this._totalSessionCount }); super.dispose(); } public get debugLogsDir(): URI | undefined { return this._getDebugLogsDir(); } private _getDebugLogsDir(): URI | undefined { if (this._debugLogsDirUri) { return this._debugLogsDirUri; } const storageUri = this._extensionContext.storageUri as URI | undefined; if (!storageUri) { return undefined; } this._debugLogsDirUri = URI.joinPath(storageUri, DEBUG_LOGS_DIR_NAME); return this._debugLogsDirUri; } async startSession(sessionId: string): Promise { this._ensureSession(sessionId, /* hasOwnSpans */ true); } /** * Synchronously ensure a session exists for buffering. Directory creation * and old-log cleanup are deferred to the first flush. * * Sessions are organized in directories: * - Parent session: `debug-logs//main.jsonl` * - Child session: `debug-logs//