diff --git a/.github/CODENOTIFY b/.github/CODENOTIFY index 60bd1fbce0ea3..805ff8fa9da6b 100644 --- a/.github/CODENOTIFY +++ b/.github/CODENOTIFY @@ -1,6 +1,5 @@ # Base Utilities src/vs/base/common/oauth.ts @TylerLeonhardt -src/vs/base/common/uri.ts @jrieken src/vs/base/browser/domSanitize.ts @mjbvz src/vs/base/parts/quickinput/** @TylerLeonhardt @@ -18,13 +17,6 @@ src/vs/platform/quickinput/** @TylerLeonhardt src/vs/platform/secrets/** @TylerLeonhardt src/vs/platform/terminal/electron-main/** @anthonykim1 src/vs/platform/terminal/node/** @anthonykim1 -src/vs/platform/actions/common/menuService.ts @jrieken -src/vs/platform/instantiation/** @jrieken - -# Editor Core -src/vs/editor/contrib/snippet/** @jrieken -src/vs/editor/contrib/suggest/** @jrieken -src/vs/editor/contrib/format/** @jrieken # Electron Main src/vs/code/** @deepak1556 @@ -74,9 +66,5 @@ extensions/git/** @lszomoru extensions/git-base/** @lszomoru extensions/github/** @lszomoru -# Chat Editing, Inline Chat -src/vs/workbench/contrib/chat/browser/chatEditing/** @jrieken -src/vs/workbench/contrib/inlineChat/** @jrieken - # Testing test/sanity/** @dmitrivMS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9d23fc89077a0..68e1f902d414e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -16,5 +16,5 @@ build/lib/policies/policyData.jsonc @joshspicer @rebornix @joaomoreno @pwang347 # VS Code API # Ensure the API team is aware of changes to the vscode-dts file # this is only about the final API, not about proposed API changes -src/vscode-dts/vscode.d.ts @jrieken @mjbvz @alexr00 -src/vs/workbench/services/extensions/common/extensionPoints.json @jrieken @mjbvz @alexr00 +src/vscode-dts/vscode.d.ts @mjbvz @alexr00 +src/vs/workbench/services/extensions/common/extensionPoints.json @mjbvz @alexr00 diff --git a/.github/classifier.json b/.github/classifier.json index 39ebd9e38b222..21a1f237181d4 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -6,24 +6,24 @@ }, "labels": { "accessibility": { "assign": ["meganrogge"]}, - "api": {"assign": ["jrieken"]}, + "api": {"assign": []}, "api-finalization": {"assign": []}, - "api-proposal": {"assign": ["jrieken"]}, + "api-proposal": {"assign": []}, "authentication": {"assign": ["TylerLeonhardt"]}, - "bisect-ext": {"assign": ["jrieken"]}, + "bisect-ext": {"assign": []}, "bot-proposal": {"assign": ["lramos15"]}, "bracket-pair-colorization": {"assign": ["hediet"]}, "bracket-pair-guides": {"assign": ["hediet"]}, - "breadcrumbs": {"assign": ["jrieken"]}, - "callhierarchy": {"assign": ["jrieken"]}, + "breadcrumbs": {"assign": []}, + "callhierarchy": {"assign": []}, "chat-terminal": {"assign": ["meganrogge"]}, "chat-terminal-output-monitor": {"assign": ["meganrogge"]}, "chrome-devtools": {"assign": ["deepak1556"]}, "cloud-changes": {"assign": ["joyceerhl"]}, "code-cli": {"assign": ["connor4312"]}, - "code-lens": {"assign": ["jrieken"]}, + "code-lens": {"assign": []}, "code-server-web": {"assign": ["aeschli"]}, - "command-center": {"assign": ["jrieken"]}, + "command-center": {"assign": []}, "comments": {"assign": ["alexr00"]}, "config": {"assign": ["sandy081"]}, "containers": {"assign": ["chrmarti"]}, @@ -58,7 +58,7 @@ "editor-indent-guides": {"assign": ["hediet"]}, "editor-input": {"assign": ["aiday-mar"]}, "editor-input-IME": {"assign": ["aiday-mar"]}, - "editor-insets": {"assign": ["jrieken"]}, + "editor-insets": {"assign": []}, "editor-minimap": {"assign": ["alexdima"]}, "editor-multicursor": {"assign": ["alexdima"]}, "editor-parameter-hints": {"assign": ["mjbvz"]}, @@ -68,7 +68,7 @@ "editor-scrollbar": {"assign": ["alexdima"]}, "editor-sorting": {"assign": ["alexdima"]}, "editor-sticky-scroll": {"assign": ["aiday-mar"]}, - "editor-symbols": {"assign": ["jrieken"]}, + "editor-symbols": {"assign": []}, "editor-synced-region": {"assign": ["aeschli"]}, "editor-textbuffer": {"assign": ["alexdima", "rebornix"]}, "editor-theming": {"assign": ["alexdima"]}, @@ -83,7 +83,7 @@ "extension-recommendations": {"assign": ["sandy081"]}, "extensions": {"assign": ["sandy081"]}, "extensions-development": {"assign": []}, - "file-decorations": {"assign": ["jrieken"]}, + "file-decorations": {"assign": []}, "file-encoding": {"assign": ["bpasero"]}, "file-explorer": {"assign": ["lramos15"]}, "file-glob": {"assign": ["bpasero"]}, @@ -91,7 +91,7 @@ "file-nesting": {"assign": ["lramos15"]}, "file-watcher": {"assign": ["bpasero"]}, "font-rendering": {"assign": ["rzhao271"]}, - "formatting": {"assign": ["jrieken"]}, + "formatting": {"assign": []}, "getting-started": {"assign": ["bhavyaus"]}, "ghost-text": {"assign": ["hediet"]}, "git": {"assign": ["lszomoru"]}, @@ -105,7 +105,7 @@ "icon-brand": {"assign": ["daviddossett"]}, "icons-product": {"assign": ["daviddossett"]}, "image-preview": {"assign": ["mjbvz"]}, - "inlay-hints": {"assign": ["jrieken", "hediet"]}, + "inlay-hints": {"assign": ["hediet"]}, "inline-completions": {"assign": ["hediet"]}, "install-update": {"assign": ["joaomoreno"], "accuracy": 0.85}, "intellisense-config": {"assign": ["rzhao271"]}, @@ -124,7 +124,7 @@ "l10n-platform": {"assign": ["TylerLeonhardt"]}, "label-provider": {"assign": ["lramos15"]}, "languages-basic": {"assign": ["aeschli"]}, - "languages-diagnostics": {"assign": ["jrieken"]}, + "languages-diagnostics": {"assign": []}, "languages-guessing": {"assign": ["TylerLeonhardt"]}, "layout": {"assign": ["benibenj"]}, "lcd-text-rendering": {"assign": []}, @@ -136,7 +136,7 @@ "menus": {"assign": ["sbatten"]}, "merge-conflict": {"assign": ["chrmarti"]}, "merge-editor": {"assign": ["hediet"]}, - "merge-editor-workbench": {"assign": ["jrieken"]}, + "merge-editor-workbench": {"assign": []}, "monaco-editor": {"assign": []}, "native-file-dialog": {"assign": ["deepak1556"]}, "network": {"assign": ["deepak1556"]}, @@ -179,7 +179,7 @@ "notebook-workflow": {"assign": []}, "open-editors": {"assign": ["lramos15"]}, "opener": {"assign": ["mjbvz"]}, - "outline": {"assign": ["jrieken"]}, + "outline": {"assign": []}, "output": {"assign": ["sandy081"]}, "perf": {"assign": []}, "perf-bloat": {"assign": []}, @@ -189,13 +189,13 @@ "proxy": {"assign": ["chrmarti"]}, "quick-open": {"assign": ["TylerLeonhardt"]}, "quick-pick": {"assign": ["TylerLeonhardt"]}, - "references-viewlet": {"assign": ["jrieken"]}, + "references-viewlet": {"assign": []}, "release-notes": {"assign": []}, "remote": {"assign": []}, "remote-connection": {"assign": ["alexdima"]}, "remote-explorer": {"assign": ["alexr00"]}, "remote-tunnel": {"assign": ["aeschli", "connor4312"]}, - "rename": {"assign": ["jrieken"]}, + "rename": {"assign": []}, "runCommands": {"assign": ["ulugbekna"]}, "sandbox": {"assign": ["deepak1556"]}, "sash-widget": {"assign": ["joaomoreno"]}, @@ -213,11 +213,11 @@ "settings-sync-server": {"assign": ["rzhao271"]}, "shared-process": {"assign": []}, "simple-file-dialog": {"assign": ["alexr00"]}, - "smart-select": {"assign": ["jrieken"]}, + "smart-select": {"assign": []}, "snap": {"assign": ["deepak1556"]}, - "snippets": {"assign": ["jrieken"]}, + "snippets": {"assign": []}, "splitview-widget": {"assign": ["joaomoreno"]}, - "suggest": {"assign": ["jrieken"]}, + "suggest": {"assign": []}, "table-widget": {"assign": ["joaomoreno"]}, "tasks": {"assign": ["meganrogge"], "accuracy": 0.85}, "telemetry": {"assign": ["lramos15"]}, @@ -263,11 +263,11 @@ "tree-sticky-scroll": {"assign": ["benibenj"]}, "tree-views": {"assign": ["alexr00"]}, "tree-widget": {"assign": ["joaomoreno"]}, - "typehierarchy": {"assign": ["jrieken"]}, + "typehierarchy": {"assign": []}, "typescript": {"assign": ["mjbvz"]}, "undo-redo": {"assign": ["alexdima"]}, "unicode-highlight": {"assign": ["hediet"]}, - "uri": {"assign": ["jrieken"]}, + "uri": {"assign": []}, "user-profiles": {"assign": ["sandy081"]}, "ux": {"assign": ["daviddossett"]}, "variable-resolving": {"assign": ["alexr00"]}, @@ -299,7 +299,7 @@ "workbench-multiroot": {"assign": ["bpasero"]}, "workbench-notifications": {"assign": ["bpasero"]}, "workbench-os-integration": {"assign": ["bpasero"]}, - "workbench-rapid-render": {"assign": ["jrieken"]}, + "workbench-rapid-render": {"assign": []}, "workbench-run-as-admin": {"assign": ["bpasero"]}, "workbench-state": {"assign": ["bpasero"]}, "workbench-status": {"assign": ["bpasero"]}, @@ -311,7 +311,7 @@ "workbench-window": {"assign": ["bpasero"]}, "workbench-workspace": {"assign": []}, "workbench-zen": {"assign": ["benibenj"]}, - "workspace-edit": {"assign": ["jrieken"]}, + "workspace-edit": {"assign": []}, "workspace-symbols": {"assign": []}, "workspace-trust": {"assign": ["lszomoru", "sbatten"]}, "zoom": {"assign": ["alexdima"] } diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index 118d9d2b60f69..8df553bc2b79d 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -1,7 +1,7 @@ jobs: - job: Web displayName: Web - timeoutInMinutes: 30 + timeoutInMinutes: 45 pool: name: 1es-ubuntu-22.04-x64 os: linux diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index a46bef0bbab71..ce2c2f708dd47 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -1041,6 +1041,7 @@ "--animation-angle", "--animation-opacity", "--chat-input-anim-angle", + "--chat-input-anim-duration", "--chat-send-button-anim-angle", "--chat-setup-dialog-glow-angle", "--vscode-chat-font-family", diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index e9fd55fa0d20f..6a7d00d955ac0 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -4268,16 +4268,6 @@ "experimental" ] }, - "github.copilot.chat.inlineChat.selectionRatioThreshold": { - "type": "number", - "default": 0, - "markdownDescription": "%github.copilot.config.inlineChat.selectionRatioThreshold%", - "tags": [ - "advanced", - "experimental", - "onExp" - ] - }, "github.copilot.chat.inlineChat.reasoningEffort": { "type": "string", "default": "low", diff --git a/extensions/copilot/package.nls.json b/extensions/copilot/package.nls.json index 5151887763a28..26cd798a0a55b 100644 --- a/extensions/copilot/package.nls.json +++ b/extensions/copilot/package.nls.json @@ -375,7 +375,6 @@ "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.inlineChat.reasoningEffort": "Controls the reasoning effort level for inline chat requests. Lower values result in faster responses with fewer reasoning tokens. Supported values depend on the model.", "github.copilot.config.inlineChat.enableThinking": "Controls whether thinking/reasoning is enabled for inline chat requests. When disabled, reasoning summaries are suppressed for faster responses.", "github.copilot.config.debug.requestLogger.maxEntries": "Maximum number of entries to keep in the request logger for debugging purposes.", diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts index 6898c09c7b387..1805638b146a6 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCli.ts @@ -278,6 +278,8 @@ export interface CLIAgentInfo { readonly agent: Readonly; /** File URI for prompt-file agents, synthetic `copilotcli:` URI for SDK-only agents. */ readonly sourceUri: URI; + readonly extensionId?: string; + readonly pluginUri?: URI; } export interface ICopilotCLIAgents { diff --git a/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLICustomizationProvider.ts b/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLICustomizationProvider.ts index d0af1a6f79dc7..95ff49d8534e5 100644 --- a/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLICustomizationProvider.ts +++ b/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/copilotCLICustomizationProvider.ts @@ -189,6 +189,8 @@ export class CopilotCLICustomizationProvider extends Disposable implements vscod uri: s.uri, type: vscode.ChatSessionCustomizationType.Skill, name: s.name, + extensionId: s.extensionId, + pluginUri: s.pluginUri, })); } diff --git a/extensions/copilot/src/extension/conversation/common/languageModelAccess.ts b/extensions/copilot/src/extension/conversation/common/languageModelAccess.ts index 0f21892246fd4..b10422f8f3da2 100644 --- a/extensions/copilot/src/extension/conversation/common/languageModelAccess.ts +++ b/extensions/copilot/src/extension/conversation/common/languageModelAccess.ts @@ -32,23 +32,21 @@ export function getModelCapabilitiesDescription(endpoint: IChatEndpoint | Langua // GPT models if (family.includes('gpt') || name.includes('gpt') || family.includes('codex') || name.includes('codex')) { if (name.includes('codex') || family.includes('codex')) { - if (name.includes('max')) { - return l10n.t('Maximum capability Codex model optimized for complex multi-file refactoring and large codebase understanding.'); - } - if (name.includes('mini')) { - return l10n.t('Lightweight Codex model for quick code completions and simple edits with low latency.'); - } return l10n.t('OpenAI Codex model specialized for code generation, debugging, and software development tasks.'); } + if (name.includes('mini')) { + return l10n.t('Lightweight GPT model for quick responses and simple tasks with low latency.'); + } + if (name.includes('copilot')) { + return l10n.t('GPT model fine-tuned for Copilot code completions.'); + } if (name.includes('4o')) { return l10n.t('Optimized GPT-4 model with faster responses and multimodal capabilities.'); } - if (name.includes('4.1') || name.includes('4-1')) { + if (name.includes('4.1')) { return l10n.t('Enhanced GPT-4 model with improved instruction following and coding performance.'); } - if (name.includes('4')) { - return l10n.t('Reliable GPT-4 model suitable for a wide range of coding and general tasks.'); - } + return l10n.t('OpenAI GPT model for coding and general assistance.'); } // Gemini models @@ -62,12 +60,9 @@ export function getModelCapabilitiesDescription(endpoint: IChatEndpoint | Langua return l10n.t('Google Gemini model with balanced performance for coding and general assistance.'); } - // o1/o3 reasoning models - if (family.includes('o1') || family.includes('o3') || name.includes('o1') || name.includes('o3')) { - if (name.includes('mini')) { - return l10n.t('Compact reasoning model for quick problem-solving with step-by-step thinking.'); - } - return l10n.t('Advanced reasoning model that excels at complex problem-solving, math, and coding challenges.'); + // Grok models + if (family.includes('grok') || name.includes('grok')) { + return l10n.t('xAI Grok model optimized for fast code generation and development tasks.'); } return undefined; diff --git a/extensions/copilot/src/extension/inlineChat/test/vscode-node/inlineChat.test.ts b/extensions/copilot/src/extension/inlineChat/test/vscode-node/inlineChat.test.ts deleted file mode 100644 index 6111a174626c7..0000000000000 --- a/extensions/copilot/src/extension/inlineChat/test/vscode-node/inlineChat.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 * as vscode from 'vscode'; -import { DisposableStore } from '../../../../util/vs/base/common/lifecycle'; - -suite('Inline Chat', function () { - // this.timeout(1000 * 60 * 1); // 1 minute - - let store: DisposableStore; - - teardown(function () { - store.dispose(); - }); - - setup(function () { - store = new DisposableStore(); - }); - - test.skip('E2E Inline Chat Test', async function () { - store.add(vscode.lm.registerLanguageModelChatProvider('test', new class implements vscode.LanguageModelChatProvider { - async provideLanguageModelChatInformation(options: { silent: boolean }, token: vscode.CancellationToken): Promise { - return [{ - id: 'test', - name: 'test', - family: 'test', - version: '0.0.0', - maxInputTokens: 1000, - maxOutputTokens: 1000, - requiresAuthorization: true, - capabilities: {} - }]; - } - async provideLanguageModelChatResponse(model: vscode.LanguageModelChatInformation, messages: Array, options: vscode.ProvideLanguageModelChatResponseOptions, progress: vscode.Progress, token: vscode.CancellationToken): Promise { - throw new Error('Method not implemented.'); - } - async provideTokenCount(model: vscode.LanguageModelChatInformation, text: string | vscode.LanguageModelChatMessage | vscode.LanguageModelChatMessage2, token: vscode.CancellationToken): Promise { - return 0; - } - })); - - - - // Create and open a new file - const document = await vscode.workspace.openTextDocument({ language: 'javascript' }); - await vscode.window.showTextDocument(document); - - try { - - await vscode.commands.executeCommand('vscode.editorChat.start', { - blockOnResponse: true, - autoSend: true, - message: 'Write me a for loop in javascript', - position: new vscode.Position(0, 0), - initialSelection: new vscode.Selection(0, 0, 0, 0), - modelSelector: { id: 'test' } - }); - } catch (err) { - assert.ok(false); - } - - }); -}); diff --git a/extensions/copilot/src/extension/inlineChat/node/inlineChatIntent.ts b/extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts similarity index 74% rename from extensions/copilot/src/extension/inlineChat/node/inlineChatIntent.ts rename to extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts index 0ac62436d769e..c5575cf6dfed1 100644 --- a/extensions/copilot/src/extension/inlineChat/node/inlineChatIntent.ts +++ b/extensions/copilot/src/extension/inlineChat2/node/inlineChatIntent.ts @@ -8,7 +8,6 @@ import { Raw } from '@vscode/prompt-tsx'; import { BudgetExceededError } from '@vscode/prompt-tsx/dist/base/materialized'; import type * as vscode from 'vscode'; import { IAuthenticationService } from '../../../platform/authentication/common/authentication'; -import { IResponsePart } from '../../../platform/chat/common/chatMLFetcher'; import { CanceledResult, ChatFetchResponseType, ChatLocation, ChatResponse, getErrorDetailsFromChatFetchError } from '../../../platform/chat/common/commonTypes'; import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { IEditSurvivalTrackerService } from '../../../platform/editSurvivalTracking/common/editSurvivalTrackerService'; @@ -16,18 +15,14 @@ import { IEndpointProvider } from '../../../platform/endpoint/common/endpointPro import { IOctoKitService } from '../../../platform/github/common/githubService'; import { IIgnoreService } from '../../../platform/ignore/common/ignoreService'; import { ILogService } from '../../../platform/log/common/logService'; -import { Prediction } from '../../../platform/networking/common/fetch'; import { IChatEndpoint, IMakeChatRequestOptions } from '../../../platform/networking/common/networking'; -import { IParserService } from '../../../platform/parser/node/parserService'; -import { getWasmLanguage } from '../../../platform/parser/node/treeSitterLanguages'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { ChatResponseStreamImpl } from '../../../util/common/chatResponseStreamImpl'; import { toErrorMessage } from '../../../util/common/errorMessage'; import { isNonEmptyArray } from '../../../util/vs/base/common/arrays'; -import { AsyncIterableSource, timeout } from '../../../util/vs/base/common/async'; +import { timeout } from '../../../util/vs/base/common/async'; import { CancellationToken } from '../../../util/vs/base/common/cancellation'; import { ResourceSet } from '../../../util/vs/base/common/map'; -import { clamp } from '../../../util/vs/base/common/numbers'; import { isFalsyOrWhitespace } from '../../../util/vs/base/common/strings'; import { assertType, isDefined } from '../../../util/vs/base/common/types'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; @@ -35,7 +30,6 @@ import { ChatRequestEditorData, ChatResponseTextEditPart, LanguageModelTextPart, import { Intent } from '../../common/constants'; import { getAgentTools } from '../../intents/node/agentIntent'; import { IIntentService } from '../../intents/node/intentService'; -import { SelectionSplitKind, SummarizedDocumentData, SummarizedDocumentSplitMetadata } from '../../intents/node/testIntent/summarizedDocumentWithSelection'; import { ChatVariablesCollection } from '../../prompt/common/chatVariablesCollection'; import { Conversation, Turn } from '../../prompt/common/conversation'; import { IToolCall } from '../../prompt/common/intents'; @@ -43,16 +37,14 @@ import { ToolCallRound } from '../../prompt/common/toolCallRound'; import { ChatTelemetryBuilder, InlineChatTelemetry } from '../../prompt/node/chatParticipantTelemetry'; import { DefaultIntentRequestHandler } from '../../prompt/node/defaultIntentRequestHandler'; import { IDocumentContext } from '../../prompt/node/documentContext'; -import { IIntent, NoopReplyInterpreter, ReplyInterpreterMetaData, TelemetryData } from '../../prompt/node/intents'; -import { ResponseProcessorContext } from '../../prompt/node/responseProcessorContext'; +import { IIntent } from '../../prompt/node/intents'; import { PromptRenderer } from '../../prompts/node/base/promptRenderer'; -import { ICompletedToolCallRound, InlineChat2Prompt, LARGE_FILE_LINE_THRESHOLD } from '../../prompts/node/inline/inlineChat2Prompt'; -import { InlineChatEditCodePrompt } from '../../prompts/node/inline/inlineChatEditCodePrompt'; +import { ICompletedToolCallRound, InlineChat2Prompt, LARGE_FILE_LINE_THRESHOLD } from './inlineChatPrompt'; import { ToolName } from '../../tools/common/toolNames'; import { CopilotToolMode } from '../../tools/common/toolsRegistry'; import { isToolValidationError, isValidatedToolInput, IToolsService } from '../../tools/common/toolsService'; -import { InlineChatProgressMessages } from './progressMessages'; -import { CopilotInteractiveEditorResponse, InteractionOutcome, InteractionOutcomeComputer } from './promptCraftingTypes'; +import { InlineChatProgressMessages } from '../../inlineChat/node/progressMessages'; +import { CopilotInteractiveEditorResponse, InteractionOutcome } from '../../inlineChat/node/promptCraftingTypes'; const INLINE_CHAT_EXIT_TOOL_NAME = 'inline_chat_exit'; @@ -64,20 +56,12 @@ interface IInlineChatEditResult { errorMessage?: string; } -interface IInlineChatEditStrategy { - executeEdit(endpoint: IChatEndpoint, conversation: Conversation, request: vscode.ChatRequest, stream: vscode.ChatResponseStream, token: CancellationToken, documentContext: IDocumentContext, chatTelemetry: ChatTelemetryBuilder): Promise; -} export class InlineChatIntent implements IIntent { static readonly ID = Intent.InlineChat; - static readonly _EDIT_TOOLS = new Set([ - ToolName.ApplyPatch, - ToolName.EditFile, - ToolName.ReplaceString, - ToolName.MultiReplaceString, - ]); + readonly id = InlineChatIntent.ID; @@ -97,8 +81,6 @@ export class InlineChatIntent implements IIntent { @IEditSurvivalTrackerService private readonly _editSurvivalTrackerService: IEditSurvivalTrackerService, @IIntentService private readonly _intentService: IIntentService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @IParserService private readonly _parserService: IParserService, - @IExperimentationService private readonly _experimentationService: IExperimentationService, @IOctoKitService private readonly _octoKitService: IOctoKitService, ) { this._progressMessages = this._instantiationService.createInstance(InlineChatProgressMessages); @@ -201,22 +183,6 @@ export class InlineChatIntent implements IIntent { } }); - // Don't use edit tools when the selection seems good enough - let useToolsForEdit = true; - const selectionRatioThreshold = clamp(this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.InlineChatSelectionRatioThreshold, this._experimentationService), 0, 1); - if (!documentContext.selection.isEmpty - && selectionRatioThreshold > 0 - && getWasmLanguage(documentContext.document.languageId) - ) { - const data = await SummarizedDocumentData.create(this._parserService, documentContext.document, documentContext.fileIndentInfo, documentContext.selection, SelectionSplitKind.Adjusted); - const { adjusted, original } = data.offsetSelections; - const ratio = original.length / adjusted.length; - if (ratio <= 1 && ratio >= selectionRatioThreshold) { - request = { ...request, command: Intent.Edit }; - useToolsForEdit = false; - } - } - // Start generating contextual message immediately const contextualMessagePromise = this._progressMessages.getContextualMessage(request.prompt, documentContext, token); @@ -228,11 +194,9 @@ export class InlineChatIntent implements IIntent { let result: IInlineChatEditResult; try { - const strategy: IInlineChatEditStrategy = useToolsForEdit - ? this._instantiationService.createInstance(InlineChatEditToolsStrategy, this) - : this._instantiationService.createInstance(InlineChatEditHeuristicStrategy, this); + const inlineToolLoop = this._instantiationService.createInstance(InlineChatToolCalling, this); - result = await strategy.executeEdit(endpoint, conversation, request, stream, token, documentContext, chatTelemetry); + result = await inlineToolLoop.run(endpoint, conversation, request, stream, token, documentContext, chatTelemetry); } catch (err) { this._logService.error(err, 'InlineChatIntent: prompt rendering failed'); return { @@ -294,11 +258,14 @@ export class InlineChatIntent implements IIntent { } } -class InlineChatEditToolsStrategy implements IInlineChatEditStrategy { +class InlineChatToolCalling { - readonly id = InlineChatIntent.ID; - readonly locations = [ChatLocation.Editor]; - readonly description = ''; + private static readonly _EDIT_TOOLS = new Set([ + ToolName.ApplyPatch, + ToolName.EditFile, + ToolName.ReplaceString, + ToolName.MultiReplaceString, + ]); constructor( private readonly _intent: InlineChatIntent, @@ -309,7 +276,7 @@ class InlineChatEditToolsStrategy implements IInlineChatEditStrategy { @IExperimentationService private readonly _experimentationService: IExperimentationService, ) { } - async executeEdit(endpoint: IChatEndpoint, conversation: Conversation, request: vscode.ChatRequest, stream: vscode.ChatResponseStream, token: CancellationToken, documentContext: IDocumentContext, chatTelemetry: ChatTelemetryBuilder): Promise { + async run(endpoint: IChatEndpoint, conversation: Conversation, request: vscode.ChatRequest, stream: vscode.ChatResponseStream, token: CancellationToken, documentContext: IDocumentContext, chatTelemetry: ChatTelemetryBuilder): Promise { assertType(request.location2 instanceof ChatRequestEditorData); assertType(documentContext); @@ -550,7 +517,7 @@ class InlineChatEditToolsStrategy implements IInlineChatEditStrategy { assertType(request.location2 instanceof ChatRequestEditorData); - const enabledTools = new Set(InlineChatIntent._EDIT_TOOLS); + const enabledTools = new Set(InlineChatToolCalling._EDIT_TOOLS); if (!request.location2.selection.isEmpty) { // only used the multi-replace when there is no selection enabledTools.delete(ToolName.MultiReplaceString); @@ -596,103 +563,3 @@ class InlineChatEditToolsStrategy implements IInlineChatEditStrategy { return result; } } - -class InlineChatEditHeuristicStrategy implements IInlineChatEditStrategy { - - readonly id = InlineChatIntent.ID; - readonly locations = [ChatLocation.Editor]; - readonly description = ''; - - constructor( - private readonly _intent: InlineChatIntent, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IExperimentationService private readonly _experimentationService: IExperimentationService, - ) { } - - async executeEdit(endpoint: IChatEndpoint, conversation: Conversation, request: vscode.ChatRequest, stream: vscode.ChatResponseStream, token: CancellationToken, documentContext: IDocumentContext, chatTelemetry: ChatTelemetryBuilder): Promise { - - assertType(request.location2 instanceof ChatRequestEditorData); - - const outcomeComputer = new InteractionOutcomeComputer(request.location2.document.uri); - const renderer = PromptRenderer.create(this._instantiationService, endpoint, InlineChatEditCodePrompt, { - ignoreCustomInstructions: true, - documentContext, - promptContext: { - query: request.prompt, - chatVariables: new ChatVariablesCollection([...request.references]), - history: conversation.turns.slice(0, -1), - } - }); - - const renderResult = await renderer.render(undefined, token, { trace: true }); - - const replyInterpreter = renderResult.metadata.get(ReplyInterpreterMetaData)?.replyInterpreter ?? new NoopReplyInterpreter(); - const telemetryData = renderResult.metadata.getAll(TelemetryData); - - const telemetry = chatTelemetry.makeRequest(this._intent, ChatLocation.Editor, conversation, renderResult.messages, renderResult.tokenCount, renderResult.references, endpoint, telemetryData, 0, 0); - - stream = ChatResponseStreamImpl.spy(stream, part => { - if (part instanceof ChatResponseTextEditPart) { - telemetry.markEmittedEdits(part.uri, part.edits); - } - }); - - let prediction: Prediction | undefined; - const documentSplit = renderResult.metadata.get(SummarizedDocumentSplitMetadata)?.split; - if (documentSplit) { - prediction = { - type: 'content', - content: '' - }; - prediction.content = `\`\`\`${documentContext.document.languageId}\n${documentSplit.codeSelected}\n\`\`\``; - } - - const source = new AsyncIterableSource(); - const responseProcessing = replyInterpreter.processResponse(new ResponseProcessorContext(conversation.sessionId, conversation.getLatestTurn(), renderResult.messages, outcomeComputer), source.asyncIterable, stream, token); - - const fetchResult = await endpoint.makeChatRequest2({ - debugName: 'InlineChat2Intent', - messages: renderResult.messages, - userInitiatedRequest: true, - location: ChatLocation.Editor, - modelCapabilities: { - enableThinking: this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.InlineChatEnableThinking, this._experimentationService), - reasoningEffort: typeof request.modelConfiguration?.reasoningEffort === 'string' - ? request.modelConfiguration.reasoningEffort - : this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.InlineChatReasoningEffort, this._experimentationService), - }, - telemetryProperties: { - messageId: telemetry.telemetryMessageId, - conversationId: telemetry.sessionId, - messageSource: this._intent.id - }, - requestOptions: { - stream: true, - prediction - }, - finishedCb: async (_text, _index, delta) => { - telemetry.markReceivedToken(); - source.emitOne({ delta }); - return undefined; - } - }, token); - - source.resolve(); - - await responseProcessing; - - const responseText = fetchResult.type === ChatFetchResponseType.Success ? fetchResult.value : ''; - telemetry.sendTelemetry( - fetchResult.requestId, fetchResult.type, responseText, - new InteractionOutcome(telemetry.editCount > 0 ? 'inlineEdit' : 'none', []), - [] - ); - - return { - needsExitTool: telemetry.editCount === 0 && fetchResult.type === ChatFetchResponseType.Success, - lastResponse: fetchResult, - telemetry, - }; - } -} diff --git a/extensions/copilot/src/extension/prompts/node/inline/inlineChat2Prompt.tsx b/extensions/copilot/src/extension/inlineChat2/node/inlineChatPrompt.tsx similarity index 93% rename from extensions/copilot/src/extension/prompts/node/inline/inlineChat2Prompt.tsx rename to extensions/copilot/src/extension/inlineChat2/node/inlineChatPrompt.tsx index 2fc270c057615..c06ea2c82b740 100644 --- a/extensions/copilot/src/extension/prompts/node/inline/inlineChat2Prompt.tsx +++ b/extensions/copilot/src/extension/inlineChat2/node/inlineChatPrompt.tsx @@ -6,18 +6,18 @@ import { AssistantMessage, PromptElement, PromptElementProps, PromptReference, PromptSizing, SystemMessage, ToolMessage, useKeepWith, UserMessage } from '@vscode/prompt-tsx'; import { ChatResponsePart } from '@vscode/prompt-tsx/dist/base/vscodeTypes'; import type { CancellationToken, ExtendedLanguageModelToolResult, Position, Progress } from 'vscode'; -import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot'; -import { CacheType } from '../../../../platform/endpoint/common/endpointTypes'; -import { IPromptPathRepresentationService } from '../../../../platform/prompts/common/promptPathRepresentationService'; -import { ChatRequest, ChatRequestEditorData, Range } from '../../../../vscodeTypes'; -import { ChatVariablesCollection } from '../../../prompt/common/chatVariablesCollection'; -import { IToolCall } from '../../../prompt/common/intents'; -import { CopilotIdentityRules } from '../base/copilotIdentity'; -import { SafetyRules } from '../base/safetyRules'; -import { Tag } from '../base/tag'; -import { ChatVariables, UserQuery } from '../panel/chatVariables'; -import { CodeBlock } from '../panel/safeElements'; -import { ToolResult } from '../panel/toolCalling'; +import { TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot'; +import { CacheType } from '../../../platform/endpoint/common/endpointTypes'; +import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService'; +import { ChatRequest, ChatRequestEditorData, Range } from '../../../vscodeTypes'; +import { ChatVariablesCollection } from '../../prompt/common/chatVariablesCollection'; +import { IToolCall } from '../../prompt/common/intents'; +import { CopilotIdentityRules } from '../../prompts/node/base/copilotIdentity'; +import { SafetyRules } from '../../prompts/node/base/safetyRules'; +import { Tag } from '../../prompts/node/base/tag'; +import { ChatVariables, UserQuery } from '../../prompts/node/panel/chatVariables'; +import { CodeBlock } from '../../prompts/node/panel/safeElements'; +import { ToolResult } from '../../prompts/node/panel/toolCalling'; /** diff --git a/extensions/copilot/src/extension/prompts/node/inline/test/inlineChat2Prompt.spec.tsx b/extensions/copilot/src/extension/inlineChat2/test/node/inlineChat2Prompt.spec.tsx similarity index 98% rename from extensions/copilot/src/extension/prompts/node/inline/test/inlineChat2Prompt.spec.tsx rename to extensions/copilot/src/extension/inlineChat2/test/node/inlineChat2Prompt.spec.tsx index 77be8ce02991c..24d7db6ebca5d 100644 --- a/extensions/copilot/src/extension/prompts/node/inline/test/inlineChat2Prompt.spec.tsx +++ b/extensions/copilot/src/extension/inlineChat2/test/node/inlineChat2Prompt.spec.tsx @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { expect, suite, test } from 'vitest'; -import { TextDocumentSnapshot } from '../../../../../platform/editing/common/textDocumentSnapshot'; -import { createTextDocumentData, setDocText } from '../../../../../util/common/test/shims/textDocument'; -import { URI } from '../../../../../util/vs/base/common/uri'; -import { ExtendedLanguageModelToolResult, LanguageModelTextPart, LanguageModelToolResult, Position, Range } from '../../../../../vscodeTypes'; -import { FileContextElement, FileSelectionElement, ICompletedToolCallRound, LARGE_FILE_LINE_THRESHOLD, ToolCallRoundsElement } from '../inlineChat2Prompt'; +import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot'; +import { createTextDocumentData, setDocText } from '../../../../util/common/test/shims/textDocument'; +import { URI } from '../../../../util/vs/base/common/uri'; +import { ExtendedLanguageModelToolResult, LanguageModelTextPart, LanguageModelToolResult, Position, Range } from '../../../../vscodeTypes'; +import { FileContextElement, FileSelectionElement, ICompletedToolCallRound, LARGE_FILE_LINE_THRESHOLD, ToolCallRoundsElement } from '../../node/inlineChatPrompt'; function createSnapshot(content: string, languageId: string = 'typescript'): TextDocumentSnapshot { const uri = URI.file('/workspace/file.ts'); diff --git a/extensions/copilot/src/extension/inlineChat/test/vscode-node/naturalLanguageHint.test.ts b/extensions/copilot/src/extension/inlineEdits/test/vscode-node/naturalLanguageHint.test.ts similarity index 100% rename from extensions/copilot/src/extension/inlineChat/test/vscode-node/naturalLanguageHint.test.ts rename to extensions/copilot/src/extension/inlineEdits/test/vscode-node/naturalLanguageHint.test.ts diff --git a/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineCompletionProvider.ts b/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineCompletionProvider.ts index f59c7a000a97d..f19d7d44be50f 100644 --- a/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineCompletionProvider.ts +++ b/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineCompletionProvider.ts @@ -35,7 +35,7 @@ import { autorun, IObservable, observableFromEvent } from '../../../util/vs/base import { basename } from '../../../util/vs/base/common/path'; import { StringEdit } from '../../../util/vs/editor/common/core/edits/stringEdit'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; -import { LineCheck } from '../../inlineChat/vscode-node/naturalLanguageHint'; +import { LineCheck } from './naturalLanguageHint'; import { createCorrelationId } from '../common/correlationId'; import { NesChangeHint } from '../common/nesTriggerHint'; import { NESInlineCompletionContext } from '../node/nextEditProvider'; diff --git a/extensions/copilot/src/extension/inlineChat/vscode-node/naturalLanguageHint.ts b/extensions/copilot/src/extension/inlineEdits/vscode-node/naturalLanguageHint.ts similarity index 100% rename from extensions/copilot/src/extension/inlineChat/vscode-node/naturalLanguageHint.ts rename to extensions/copilot/src/extension/inlineEdits/vscode-node/naturalLanguageHint.ts diff --git a/extensions/copilot/src/extension/intents/node/allIntents.ts b/extensions/copilot/src/extension/intents/node/allIntents.ts index a21db2ccafdc5..1a14e3a38defb 100644 --- a/extensions/copilot/src/extension/intents/node/allIntents.ts +++ b/extensions/copilot/src/extension/intents/node/allIntents.ts @@ -5,7 +5,7 @@ import { SyncDescriptor } from '../../../util/vs/platform/instantiation/common/descriptors'; -import { InlineChatIntent } from '../../inlineChat/node/inlineChatIntent'; +import { InlineChatIntent } from '../../inlineChat2/node/inlineChatIntent'; import { IntentRegistry } from '../../prompt/node/intentRegistry'; import { AgentIntent } from './agentIntent'; import { AskAgentIntent } from './askAgentIntent'; diff --git a/extensions/copilot/src/extension/intents/node/editCodeIntent.ts b/extensions/copilot/src/extension/intents/node/editCodeIntent.ts index 011fee98596c0..4e8cb4c0ba845 100644 --- a/extensions/copilot/src/extension/intents/node/editCodeIntent.ts +++ b/extensions/copilot/src/extension/intents/node/editCodeIntent.ts @@ -421,7 +421,6 @@ export class EditCodeIntentInvocation implements IIntentInvocation { // Don't report file references that came in via chat variables in an editing session, unless they have warnings, // because they are already displayed as part of the working set references: result.references.filter((ref) => this.shouldKeepReference(editCodeStep, ref, toolReferences, chatVariables)), - // telemetryData: result.metadata.getAll(DocumentToAstSelectionData) }; } diff --git a/extensions/copilot/src/extension/prompt/node/chatParticipantTelemetry.ts b/extensions/copilot/src/extension/prompt/node/chatParticipantTelemetry.ts index da9a241e61247..ece8b8d188047 100644 --- a/extensions/copilot/src/extension/prompt/node/chatParticipantTelemetry.ts +++ b/extensions/copilot/src/extension/prompt/node/chatParticipantTelemetry.ts @@ -25,7 +25,6 @@ import { DiagnosticsTelemetryData, findDiagnosticsTelemetry } from '../../inline import { InteractionOutcome } from '../../inlineChat/node/promptCraftingTypes'; import { AgentIntent } from '../../intents/node/agentIntent'; import { EditCodeIntent } from '../../intents/node/editCodeIntent'; -import { DocumentToAstSelectionData } from '../../prompts/node/inline/inlineChatEditCodePrompt'; import { getCustomInstructionTelemetry } from '../../prompts/node/panel/customInstructions'; import { PATCH_PREFIX } from '../../tools/node/applyPatch/parseApplyPatch'; import { ChatVariablesCollection, parseSlashCommand } from '../common/chatVariablesCollection'; @@ -205,8 +204,6 @@ type RequestInlineTelemetryMeasurements = RequestTelemetryMeasurements & { selectionProblemsCount: number; diagnosticsCount: number; selectionDiagnosticsCount: number; - userSelectionLength: number; - adjustedSelectionLength: number; }; //#endregion @@ -903,11 +900,6 @@ export class InlineChatTelemetry extends ChatTelemetry { return acc; }, {} as Record); - - const selectionData = this._getTelemetryData(DocumentToAstSelectionData); - const userSelectionLength = selectionData?.original.length ?? -1; - const adjustedSelectionLength = selectionData?.adjusted.length ?? -1; - /* __GDPR__ "inline.request" : { "owner": "digitarald", @@ -954,8 +946,6 @@ export class InlineChatTelemetry extends ChatTelemetry { "numToolCalls": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The total number of tool calls" }, "availableToolCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How number of tools that were available." }, "toolTokenCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "How many tokens were used by tool definitions." }, - "userSelectionLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The length of the user selection" }, - "adjustedSelectionLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The length of the adjusted user selection" }, "isBYOK": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Whether the request was for a BYOK model" }, "isAuto": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Whether the request was for an Auto model" } } @@ -1001,8 +991,6 @@ export class InlineChatTelemetry extends ChatTelemetry { numToolCalls: toolCalls.length, availableToolCount: this._availableToolCount, toolTokenCount: this._toolTokenCount, - userSelectionLength, - adjustedSelectionLength, isBYOK: isBYOKModel(this._endpoint), isAuto: isAutoModel(this._endpoint) } satisfies RequestInlineTelemetryMeasurements); diff --git a/extensions/copilot/src/extension/prompts/node/inline/inlineChatEditCodePrompt.tsx b/extensions/copilot/src/extension/prompts/node/inline/inlineChatEditCodePrompt.tsx index b4eb9762ec08f..3653ed26a503e 100644 --- a/extensions/copilot/src/extension/prompts/node/inline/inlineChatEditCodePrompt.tsx +++ b/extensions/copilot/src/extension/prompts/node/inline/inlineChatEditCodePrompt.tsx @@ -11,10 +11,9 @@ import { IParserService } from '../../../../platform/parser/node/parserService'; import { IExperimentationService } from '../../../../platform/telemetry/common/nullExperimentationService'; import { isNotebookCellOrNotebookChatInput } from '../../../../util/common/notebooks'; import { illegalArgument } from '../../../../util/vs/base/common/errors'; -import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange'; import { GenericInlinePromptProps } from '../../../context/node/resolvers/genericInlineIntentInvocation'; import { SelectionSplitKind, SummarizedDocumentData, SummarizedDocumentWithSelection } from '../../../intents/node/testIntent/summarizedDocumentWithSelection'; -import { EarlyStopping, LeadingMarkdownStreaming, TelemetryData } from '../../../prompt/node/intents'; +import { EarlyStopping, LeadingMarkdownStreaming } from '../../../prompt/node/intents'; import { TextPieceClassifiers } from '../../../prompt/node/streamingEdits'; import { InstructionMessage } from '../base/instructionMessage'; import { LegacySafetyRules } from '../base/safetyRules'; @@ -26,15 +25,6 @@ import { ProjectLabels } from '../panel/projectLabels'; import { LanguageServerContextPrompt } from './languageServerContextPrompt'; import { SummarizedDocumentSplit } from './promptingSummarizedDocument'; -export class DocumentToAstSelectionData extends TelemetryData { - - constructor( - readonly original: OffsetRange, - readonly adjusted: OffsetRange, - ) { - super(); - } -} export interface InlineChatEditCodePromptProps extends GenericInlinePromptProps { readonly ignoreCustomInstructions?: boolean; @@ -117,10 +107,6 @@ export class InlineChatEditCodePrompt extends PromptElement {data.hasCodeWithoutSelection && <>The modified {data.placeholderText} code with ``` is:} - ); } diff --git a/extensions/copilot/src/extension/typescriptContext/vscode-node/languageContextService.ts b/extensions/copilot/src/extension/typescriptContext/vscode-node/languageContextService.ts index 7f48633ba47c0..fdb2488c7d9d5 100644 --- a/extensions/copilot/src/extension/typescriptContext/vscode-node/languageContextService.ts +++ b/extensions/copilot/src/extension/typescriptContext/vscode-node/languageContextService.ts @@ -14,13 +14,13 @@ import { ILogService } from '../../../platform/log/common/logService'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; import { Queue } from '../../../util/vs/base/common/async'; +import { CancellationToken } from '../../../util/vs/base/common/cancellation'; import { DisposableStore } from '../../../util/vs/base/common/lifecycle'; import { generateUuid } from '../../../util/vs/base/common/uuid'; import * as protocol from '../common/serverProtocol'; import { InspectorDataProvider } from './inspector'; import { ThrottledDebouncer } from './throttledDebounce'; import { ContextItemResultBuilder, ContextItemSummary, ResolvedRunnableResult, type OnCachePopulatedEvent, type OnContextComputedEvent, type OnContextComputedOnTimeoutEvent } from './types'; -import { CancellationToken } from '../../../util/vs/base/common/cancellation'; const currentTokenBudget: number = 8 * 1024; @@ -1975,7 +1975,7 @@ export class InlineCompletionContribution implements vscode.Disposable, TokenBud } // Register with chat always. - this.registrations.add(this.languageContextProviderService.registerContextProvider(provider, [ProviderTarget.Completions])); + this.registrations.add(this.languageContextProviderService.registerContextProvider(provider, [ProviderTarget.Completions, ProviderTarget.NES])); this.telemetrySender.sendInlineCompletionProviderTelemetry(KnownSources.completion, true); logService.info('Registered TypeScript context provider with Copilot inline completions.'); } catch (error) { diff --git a/extensions/copilot/src/platform/configuration/common/configurationService.ts b/extensions/copilot/src/platform/configuration/common/configurationService.ts index 93d52257f1ba9..936bb3a2c16f7 100644 --- a/extensions/copilot/src/platform/configuration/common/configurationService.ts +++ b/extensions/copilot/src/platform/configuration/common/configurationService.ts @@ -642,7 +642,6 @@ export namespace ConfigKey { export const NotebookAlternativeDocumentFormat = defineAndMigrateExpSetting('chat.advanced.notebook.alternativeFormat', 'chat.notebook.alternativeFormat', AlternativeNotebookFormat.xml); export const UseAlternativeNESNotebookFormat = defineAndMigrateExpSetting('chat.advanced.notebook.alternativeNESFormat.enabled', 'chat.notebook.alternativeNESFormat.enabled', false); - export const InlineChatSelectionRatioThreshold = defineSetting('chat.inlineChat.selectionRatioThreshold', ConfigType.ExperimentBased, 0); export const InlineChatReasoningEffort = defineSetting('chat.inlineChat.reasoningEffort', ConfigType.ExperimentBased, 'low'); export const InlineChatEnableThinking = defineSetting('chat.inlineChat.enableThinking', ConfigType.ExperimentBased, false); diff --git a/extensions/copilot/src/platform/github/common/githubAPI.ts b/extensions/copilot/src/platform/github/common/githubAPI.ts index 86b9893e182d2..04a110a2d98db 100644 --- a/extensions/copilot/src/platform/github/common/githubAPI.ts +++ b/extensions/copilot/src/platform/github/common/githubAPI.ts @@ -7,6 +7,46 @@ import { ILogService } from '../../log/common/logService'; import { IFetcherService } from '../../networking/common/fetcherService'; import { ITelemetryService } from '../../telemetry/common/telemetry'; +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +export function getErrorCode(e: unknown): string | undefined { + if (!isObject(e)) { + return undefined; + } + + if (e.status !== undefined) { + return String(e.status); + } + + const networkError = e.networkError; + if (isObject(networkError) && networkError.statusCode !== undefined) { + return String(networkError.statusCode); + } + + const graphQLErrors = e.graphQLErrors; + if (Array.isArray(graphQLErrors)) { + const firstGraphQLError = graphQLErrors[0]; + if (isObject(firstGraphQLError)) { + const extensions = firstGraphQLError.extensions; + if (isObject(extensions) && extensions.code !== undefined) { + return String(extensions.code); + } + } + } + + if (e.code !== undefined) { + return String(e.code); + } + + if (typeof e.name === 'string' && e.name) { + return e.name; + } + + return undefined; +} + export interface PullRequestSearchItem { id: string; number: number; @@ -366,6 +406,24 @@ export async function getPullRequestFromGlobalId( const node = result?.data?.node; logService.debug(`[GitHubAPI] GetPullRequestGlobal: host=${host}, globalId=${globalId}, found=${!!node}, prNumber=${node?.number}, errors=${JSON.stringify(result?.errors)}`); + + if (!node) { + const properties: { errorCode?: string; requestFailed: string } = { + requestFailed: String(result === undefined), + }; + const errorCode = getErrorCode(result?.errors?.[0]); + if (errorCode) { + properties.errorCode = errorCode; + } + /* __GDPR__ + "pr.getPullRequestFromGlobalIdFailed" : { + "errorCode": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "requestFailed": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } + } + */ + telemetry.sendMSFTTelemetryErrorEvent('pr.getPullRequestFromGlobalIdFailed', properties); + } + return node; } diff --git a/extensions/copilot/src/platform/github/common/octoKitServiceImpl.ts b/extensions/copilot/src/platform/github/common/octoKitServiceImpl.ts index 2e7de8a268a14..f88d10f160f6c 100644 --- a/extensions/copilot/src/platform/github/common/octoKitServiceImpl.ts +++ b/extensions/copilot/src/platform/github/common/octoKitServiceImpl.ts @@ -9,7 +9,7 @@ import { ICAPIClientService } from '../../endpoint/common/capiClient'; import { ILogService } from '../../log/common/logService'; import { IFetcherService } from '../../networking/common/fetcherService'; import { ITelemetryService } from '../../telemetry/common/telemetry'; -import { AssignableActor, getAssignableActorsWithAssignableUsers, getAssignableActorsWithSuggestedActors, PullRequestComment, PullRequestSearchItem, SessionInfo } from './githubAPI'; +import { AssignableActor, getAssignableActorsWithAssignableUsers, getAssignableActorsWithSuggestedActors, getErrorCode, PullRequestComment, PullRequestSearchItem, SessionInfo } from './githubAPI'; import { AuthOptions, BaseOctoKitService, CCAEnabledResult, CustomAgentDetails, CustomAgentListItem, CustomAgentListOptions, ErrorResponseWithStatusCode, IOctoKitService, IOctoKitUser, JobInfo, PermissiveAuthRequiredError, PullRequestFile, RemoteAgentJobResponse } from './githubService'; export class OctoKitService extends BaseOctoKitService implements IOctoKitService { @@ -556,42 +556,4 @@ export class OctoKitService extends BaseOctoKitService implements IOctoKitServic } } -function isObject(value: unknown): value is Record { - return typeof value === 'object' && value !== null; -} - -export function getErrorCode(e: unknown): string | undefined { - if (!isObject(e)) { - return undefined; - } - - if (e.status !== undefined) { - return String(e.status); - } - - const networkError = e.networkError; - if (isObject(networkError) && networkError.statusCode !== undefined) { - return String(networkError.statusCode); - } - - const graphQLErrors = e.graphQLErrors; - if (Array.isArray(graphQLErrors)) { - const firstGraphQLError = graphQLErrors[0]; - if (isObject(firstGraphQLError)) { - const extensions = firstGraphQLError.extensions; - if (isObject(extensions) && extensions.code !== undefined) { - return String(extensions.code); - } - } - } - - if (e.code !== undefined) { - return String(e.code); - } - - if (typeof e.name === 'string' && e.name) { - return e.name; - } - return undefined; -} diff --git a/extensions/copilot/src/platform/inlineEdits/common/inlineEditLogContext.ts b/extensions/copilot/src/platform/inlineEdits/common/inlineEditLogContext.ts index f518894dcd772..9f97b63dfd834 100644 --- a/extensions/copilot/src/platform/inlineEdits/common/inlineEditLogContext.ts +++ b/extensions/copilot/src/platform/inlineEdits/common/inlineEditLogContext.ts @@ -392,7 +392,10 @@ export class InlineEditRequestLogContext { * (e.g., in `setIsCachedResult` which intentionally overrides any inherited outcome). */ private _setOutcome(outcome: LogContextOutcome): void { - if (this._outcome !== 'pending') { + // 'reusedInFlight' is an intermediate state set when joining an in-flight + // request (before the result arrives), so it can legitimately transition + // to the final outcome (skipped, errored, etc.) just like 'pending'. + if (this._outcome !== 'pending' && this._outcome !== 'reusedInFlight') { console.warn(`[InlineEditRequestLogContext] outcome transition from '${this._outcome}' to '${outcome}' (request #${this.requestId})`); } this._outcome = outcome; diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index b2af5e37e80a0..bf4043983fdb1 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -1397,7 +1397,7 @@ export class CodeApplication extends Disposable { } // Handle agents window first based on context - if ((process as INodeProcess).isEmbeddedApp || (!isLinux && args['agents'] && this.productService.quality !== 'stable')) { + if ((process as INodeProcess).isEmbeddedApp || (args['agents'] && this.productService.quality !== 'stable')) { return windowsMainService.openAgentsWindow({ context, cli: args, diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts index 22c2a2e13ff1c..7ef1bfef61f4b 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts @@ -72,23 +72,30 @@ export class MultiDiffEditorViewModel extends Disposable { const allItems = mapObservableArrayCached( this, this._documentsArr, - (d, store) => store.add(this._instantiationService.createInstance(DocumentDiffItemViewModel, d, this)) + (d, store) => store.add(RefCounted.create(this._instantiationService.createInstance(DocumentDiffItemViewModel, d, this))) ).recomputeInitiallyAndOnChange(this._store); - const waitForNewDiffs: IObservable> = derived(this, reader => { + const waitForNewDiffs: IObservable[]>> = derived(this, reader => { const next = allItems.read(reader); - const unresolved = next.filter(i => !i.waitForInitialDiffOr1s.promiseResult.read(undefined)); + const unresolved = next.filter(i => !i.object.waitForInitialDiffOr1s.promiseResult.read(undefined)); if (unresolved.length === 0) { return ObservablePromise.resolved(next); } return new ObservablePromise( - Promise.all(unresolved.map(i => i.waitForInitialDiffOr1s.promise)).then(() => next) + Promise.all(unresolved.map(i => i.object.waitForInitialDiffOr1s.promise)).then(() => next) ); }); - const resolved = new ObservableResolvedPromise(waitForNewDiffs, [] as readonly DocumentDiffItemViewModel[], this._store); + const resolved = new ObservableResolvedPromise(waitForNewDiffs, [] as readonly RefCounted[], this._store); + + this.items = derived(this, reader => { + const resolvedItems = resolved.lastResolved.read(reader); + return resolvedItems.map(i => { + const ref = reader.store.add(i.createNewRef(i)); + return ref.object; + }); + }); - this.items = resolved.lastResolved; this.isLoading = derived(this, reader => this._documents.read(reader) === 'loading' || resolved.isResolving.read(reader) ); diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 84fc789f24967..e04bf3ebe14aa 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -1368,6 +1368,7 @@ export class ActionListWidget extends Disposable { const child = group.actions[ci]; const icon = (child as IAction & { icon?: ThemeIcon }).icon ?? ThemeIcon.fromId(child.checked ? Codicon.check.id : Codicon.blank.id); + const hoverContent = (child as IAction & { hoverContent?: string }).hoverContent; submenuItems.push({ item: child, kind: ActionListItemKind.Action, @@ -1375,7 +1376,7 @@ export class ActionListWidget extends Disposable { description: child.tooltip || undefined, group: { title: '', icon }, hideIcon: false, - hover: {}, + hover: hoverContent ? { content: hoverContent } : {}, }); } if (gi < groupsWithActions.length - 1) { diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index 7979a1ef31007..18b5b0ba26cc5 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -389,14 +389,14 @@ border-radius: 5px; box-shadow: 0 2px 8px var(--vscode-widget-shadow); z-index: 50; - width: fit-content; + width: max-content; } .action-list-submenu-hover-header { padding: 4px 8px; line-height: 1.4em; font-size: 12px; - max-width: var(--vscode-hover-maxWidth, 500px); + max-width: 300px; word-wrap: break-word; } diff --git a/src/vs/platform/actions/browser/toolbar.ts b/src/vs/platform/actions/browser/toolbar.ts index e44cdb4eae07e..c6cd829925737 100644 --- a/src/vs/platform/actions/browser/toolbar.ts +++ b/src/vs/platform/actions/browser/toolbar.ts @@ -12,7 +12,7 @@ import { intersection } from '../../../base/common/collections.js'; import { BugIndicatingError } from '../../../base/common/errors.js'; import { Emitter } from '../../../base/common/event.js'; import { Iterable } from '../../../base/common/iterator.js'; -import { DisposableStore } from '../../../base/common/lifecycle.js'; +import { DisposableStore, toDisposable, IDisposable, Disposable } from '../../../base/common/lifecycle.js'; import { localize } from '../../../nls.js'; import { createActionViewItem, getActionBarActions } from './menuEntryActionViewItem.js'; import { IMenu, IMenuActionOptions, IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from '../common/actions.js'; @@ -285,6 +285,38 @@ export class WorkbenchToolBar extends ToolBar { // ---- MenuWorkbenchToolBar ------------------------------------------------- +const sharedIntersectionObservers = new WeakMap(); +const intersectionObserverCallbacks = new WeakMap void>(); + +function observeVisibility(element: Element, callback: (isVisible: boolean) => void): IDisposable { + const targetWindow = getWindow(element); + if (typeof targetWindow.IntersectionObserver !== 'function') { + // fallback: assume always visible + callback(true); + return Disposable.None; + } + + let observer = sharedIntersectionObservers.get(targetWindow); + if (!observer) { + observer = new targetWindow.IntersectionObserver((entries) => { + for (const entry of entries) { + const cb = intersectionObserverCallbacks.get(entry.target); + if (cb) { + cb(entry.isIntersecting); + } + } + }); + sharedIntersectionObservers.set(targetWindow, observer); + } + + intersectionObserverCallbacks.set(element, callback); + observer.observe(element); + + return toDisposable(() => { + intersectionObserverCallbacks.delete(element); + observer.unobserve(element); + }); +} export interface IToolBarRenderOptions { /** @@ -337,6 +369,7 @@ export class MenuWorkbenchToolBar extends WorkbenchToolBar { private readonly _menuOptions: IMenuActionOptions | undefined; private readonly _toolbarOptions: IToolBarRenderOptions | undefined; private readonly _container: HTMLElement; + private readonly _viewDisposables = this._store.add(new DisposableStore()); constructor( container: HTMLElement, @@ -374,13 +407,18 @@ export class MenuWorkbenchToolBar extends WorkbenchToolBar { // update logic this._menu = this._store.add(menuService.createMenu(menuId, contextKeyService, { emitEventsForSubmenuChanges: true, eventDebounceDelay: options?.eventDebounceDelay })); - this._store.add(this._menu.onDidChange(() => { - this._updateToolbar(); - this._onDidChangeMenuItems.fire(this); - })); - - this._store.add(actionViewService.onDidChange(e => { - if (e === menuId) { + this._store.add(observeVisibility(this._container, isVisible => { + this._viewDisposables.clear(); + if (isVisible) { + this._viewDisposables.add(this._menu.onDidChange(() => { + this._updateToolbar(); + this._onDidChangeMenuItems.fire(this); + })); + this._viewDisposables.add(actionViewService.onDidChange(e => { + if (e === menuId) { + this._updateToolbar(); + } + })); this._updateToolbar(); } })); diff --git a/src/vs/platform/contextkey/common/scanner.ts b/src/vs/platform/contextkey/common/scanner.ts index 64dff3d76fcf3..68aa701350699 100644 --- a/src/vs/platform/contextkey/common/scanner.ts +++ b/src/vs/platform/contextkey/common/scanner.ts @@ -133,7 +133,7 @@ export class Scanner { case TokenType.LtEq: return '<='; case TokenType.Gt: - return '>='; + return '>'; case TokenType.GtEq: return '>='; case TokenType.RegexOp: diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 1faa200f8ce2f..7815fdff84402 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -6,7 +6,7 @@ import { app } from 'electron'; import { coalesce } from '../../../base/common/arrays.js'; import { Emitter, Event } from '../../../base/common/event.js'; -import { IProcessEnvironment, isLinux, isMacintosh } from '../../../base/common/platform.js'; +import { IProcessEnvironment, isMacintosh } from '../../../base/common/platform.js'; import { URI } from '../../../base/common/uri.js'; import { whenDeleted } from '../../../base/node/pfs.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; @@ -164,7 +164,7 @@ export class LaunchMainService implements ILaunchMainService { } // Agents window - else if (!isLinux && args['agents'] && this.productService.quality !== 'stable') { + else if (args['agents'] && this.productService.quality !== 'stable') { usedWindows = await this.windowsMainService.openAgentsWindow(baseConfig); } diff --git a/src/vs/platform/terminal/node/terminalContrib/autoReplies/autoRepliesContribController.ts b/src/vs/platform/terminal/node/terminalContrib/autoReplies/autoRepliesContribController.ts index bf75ded3d6f56..8cf4bd13b49b1 100644 --- a/src/vs/platform/terminal/node/terminalContrib/autoReplies/autoRepliesContribController.ts +++ b/src/vs/platform/terminal/node/terminalContrib/autoReplies/autoRepliesContribController.ts @@ -55,6 +55,8 @@ export class AutoRepliesPtyServiceContribution implements IPtyServiceContributio } processAutoResponders.clear(); } + this._autoResponders.delete(persistentProcessId); + this._terminalProcesses.delete(persistentProcessId); } handleProcessInput(persistentProcessId: number, data: string) { diff --git a/src/vs/sessions/browser/media/style.css b/src/vs/sessions/browser/media/style.css index 228ae5d448f30..b800b9c8015ca 100644 --- a/src/vs/sessions/browser/media/style.css +++ b/src/vs/sessions/browser/media/style.css @@ -429,11 +429,19 @@ border-color: var(--vscode-agentsChatInput-focusBorder, var(--vscode-focusBorder)) !important; } -/* While the developer-joy animated border is active, suppress the static - border so it doesn't visually conflict with the spinning gradient ring. */ -.agent-sessions-workbench .interactive-session .chat-input-container.working, +/* While the developer-joy animated border is active, keep a faint + persistent ring so the input still has a visible boundary throughout + the comet animation (improves visuals + accessibility). The spinning + gradient ring overlays this. When focused, the regular focus border + is blended/dimmed during the animation to avoid competing visually. */ +.agent-sessions-workbench .interactive-session .chat-input-container.working:not(.focused) { + border-color: var(--vscode-agentsChatInput-border) !important; +} + +/* Dim the focus border while the comet is animating so the bright focus + ring doesn't visually fight with the spinning gradient. */ .agent-sessions-workbench .interactive-session .chat-input-container.working.focused { - border-color: transparent !important; + border-color: color-mix(in srgb, var(--vscode-agentsChatInput-focusBorder, var(--vscode-focusBorder)) 40%, transparent) !important; } /* Make the Monaco editor inside the chat input transparent so it inherits the chatInput.background */ diff --git a/src/vs/sessions/browser/menus.ts b/src/vs/sessions/browser/menus.ts index 9726373a2b977..c81d9ea0704eb 100644 --- a/src/vs/sessions/browser/menus.ts +++ b/src/vs/sessions/browser/menus.ts @@ -28,4 +28,5 @@ export const Menus = { NewSessionConfig: new MenuId('NewSessions.SessionConfigMenu'), NewSessionControl: new MenuId('NewSessions.SessionControlMenu'), NewSessionRepositoryConfig: new MenuId('NewSessions.RepositoryConfigMenu'), + SessionWorkspaceManage: new MenuId('Sessions.SessionWorkspaceManage'), } as const; diff --git a/src/vs/sessions/contrib/changes/browser/changesViewActions.ts b/src/vs/sessions/contrib/changes/browser/changesViewActions.ts index 53df074fa2fb4..9e395451c21bb 100644 --- a/src/vs/sessions/contrib/changes/browser/changesViewActions.ts +++ b/src/vs/sessions/contrib/changes/browser/changesViewActions.ts @@ -13,7 +13,7 @@ import { IViewsService } from '../../../../workbench/services/views/common/views import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js'; -import { ActiveSessionContextKeys, CHANGES_VIEW_ID } from '../common/changes.js'; +import { ActiveSessionContextKeys, CHANGES_VIEW_ID, ChangesContextKeys } from '../common/changes.js'; import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; @@ -150,12 +150,14 @@ class OpenFileAction extends Action2 { id: MenuId.ChatEditingSessionChangeToolbar, group: 'navigation', order: 1, - when: IsSessionsWindowContext, alt: { id: 'workbench.action.agentSessions.openChanges', title: localize2('openChanges', "Open Changes"), icon: Codicon.gitCompare, - } + }, + when: ContextKeyExpr.and( + IsSessionsWindowContext, + ChangesContextKeys.ChangeKind.isEqualTo('file')) } }); } diff --git a/src/vs/sessions/contrib/changes/browser/changesViewRenderer.ts b/src/vs/sessions/contrib/changes/browser/changesViewRenderer.ts index cc31d1a26a639..5ace9f05f5a7e 100644 --- a/src/vs/sessions/contrib/changes/browser/changesViewRenderer.ts +++ b/src/vs/sessions/contrib/changes/browser/changesViewRenderer.ts @@ -15,7 +15,7 @@ import { IResourceNode, ResourceTree } from '../../../../base/common/resourceTre import { URI } from '../../../../base/common/uri.js'; import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { FileKind } from '../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; @@ -24,7 +24,7 @@ import { bindContextKey } from '../../../../platform/observable/common/platformO import { IResourceLabel, ResourceLabels } from '../../../../workbench/browser/labels.js'; import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; import { isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; -import { chatEditingWidgetFileStateContextKey, ModifiedFileEntryState } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; +import { ModifiedFileEntryState } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; import { GITHUB_REMOTE_FILE_SCHEME, ISessionFileChange } from '../../../services/sessions/common/session.js'; import { ActiveSessionContextKeys, ChangesContextKeys, ChangesViewMode } from '../common/changes.js'; @@ -156,7 +156,7 @@ export function buildTreeChildren(items: IChangesFileItem[], treeRootInfo?: ICha interface IChangesTreeTemplate { readonly label: IResourceLabel; readonly toolbar: MenuWorkbenchToolBar | undefined; - readonly contextKeyService: IContextKeyService | undefined; + readonly changeKindContextKey: IContextKey<'root' | 'folder' | 'file'>; readonly reviewCommentsBadge: HTMLElement; readonly agentFeedbackBadge: HTMLElement; readonly decorationBadge: HTMLElement; @@ -218,10 +218,12 @@ export class ChangesTreeRenderer implements ICompressibleTreeRenderer, _index: number, templateData: IChangesTreeTemplate): void { @@ -261,9 +263,8 @@ export class ChangesTreeRenderer implements ICompressibleTreeRenderer, templateData: IChangesTreeTemplate): void { @@ -402,9 +401,8 @@ export class ChangesTreeRenderer implements ICompressibleTreeRenderer, _index: number, templateData: IChangesTreeTemplate): void { diff --git a/src/vs/sessions/contrib/changes/common/changes.ts b/src/vs/sessions/contrib/changes/common/changes.ts index 05f5df42f8365..82d57d7b90d34 100644 --- a/src/vs/sessions/contrib/changes/common/changes.ts +++ b/src/vs/sessions/contrib/changes/common/changes.ts @@ -27,6 +27,7 @@ export const enum IsolationMode { } export const ChangesContextKeys = { + ChangeKind: new RawContextKey<'root' | 'folder' | 'file'>('sessions.changeKind', 'file'), VersionMode: new RawContextKey('sessions.changesVersionMode', ChangesVersionMode.BranchChanges), ViewMode: new RawContextKey('sessions.changesViewMode', ChangesViewMode.List) }; diff --git a/src/vs/sessions/contrib/chat/browser/media/chatInput.css b/src/vs/sessions/contrib/chat/browser/media/chatInput.css index 9d7c730919321..e933f0e5f9363 100644 --- a/src/vs/sessions/contrib/chat/browser/media/chatInput.css +++ b/src/vs/sessions/contrib/chat/browser/media/chatInput.css @@ -239,8 +239,8 @@ justify-content: center; flex-shrink: 0; position: relative; - width: 22px; - height: 22px; + width: 23px; + height: 23px; border-radius: 4px; } @@ -248,9 +248,9 @@ display: flex; align-items: center; justify-content: center; - width: 22px; - height: 22px; - min-width: 22px; + width: 23px; + height: 23px; + min-width: 23px; padding: 0; border-radius: 4px; color: var(--vscode-icon-foreground); @@ -282,8 +282,11 @@ box-shadow: none !important; } -.monaco-workbench .sessions-chat-send-button .monaco-button .codicon[class*='codicon-'] { - font-size: 14px; +.monaco-workbench .sessions-chat-send-button .monaco-button.codicon[class*='codicon-']::before, +.monaco-workbench .sessions-chat-send-button .monaco-button .codicon[class*='codicon-']::before { + /* Optical alignment: nudge arrow glyph 1px left to visually center it. */ + display: inline-block; + transform: translateX(-0.5px); } /* Ensure no underline / link decoration ever shows under the codicon glyph @@ -352,9 +355,17 @@ } .agent-sessions-workbench .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:has(> .action-label.codicon-arrow-up) > .action-label.codicon-arrow-up { + box-sizing: border-box; + width: 23px; + height: 23px; transition: background-color 250ms ease, color 250ms ease; } +/* Optical alignment: nudge arrow glyph 1px left to visually center it. */ +.agent-sessions-workbench .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item > .action-label.codicon-arrow-up::before { + display: inline-block; +} + /* Focus indicator drawn on the action-item wrapper so it sits cleanly around the button surface with a small offset, matching the new-session button. */ .agent-sessions-workbench .interactive-session .chat-input-toolbars > .chat-execute-toolbar .monaco-action-bar .action-item:not(.disabled):has(> .action-label.codicon-arrow-up:focus-visible) { diff --git a/src/vs/sessions/contrib/chat/browser/nullInlineChatSessionService.ts b/src/vs/sessions/contrib/chat/browser/nullInlineChatSessionService.ts index 857f9033f5085..28c4fd671ed33 100644 --- a/src/vs/sessions/contrib/chat/browser/nullInlineChatSessionService.ts +++ b/src/vs/sessions/contrib/chat/browser/nullInlineChatSessionService.ts @@ -7,7 +7,7 @@ import { Event } from '../../../../base/common/event.js'; import { URI } from '../../../../base/common/uri.js'; import { IActiveCodeEditor, ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { IInlineChatSession2, IInlineChatSessionService } from '../../../../workbench/contrib/inlineChat/browser/inlineChatSessionService.js'; +import { IInlineChatSession, IInlineChatSessionService } from '../../../../workbench/contrib/inlineChat/browser/inlineChatSessionService.js'; class NullInlineChatSessionService implements IInlineChatSessionService { declare _serviceBrand: undefined; @@ -17,15 +17,15 @@ class NullInlineChatSessionService implements IInlineChatSessionService { dispose(): void { } - createSession(_editor: ICodeEditor): IInlineChatSession2 { + createSession(_editor: ICodeEditor): IInlineChatSession { throw new Error('Inline chat sessions are not supported in the sessions window'); } - getSessionByTextModel(_uri: URI): IInlineChatSession2 | undefined { + getSessionByTextModel(_uri: URI): IInlineChatSession | undefined { return undefined; } - getSessionBySessionUri(_uri: URI): IInlineChatSession2 | undefined { + getSessionBySessionUri(_uri: URI): IInlineChatSession | undefined { return undefined; } } diff --git a/src/vs/sessions/contrib/chat/browser/scopedWorkspacePicker.ts b/src/vs/sessions/contrib/chat/browser/scopedWorkspacePicker.ts index 6285305cdca05..a1932e8538fce 100644 --- a/src/vs/sessions/contrib/chat/browser/scopedWorkspacePicker.ts +++ b/src/vs/sessions/contrib/chat/browser/scopedWorkspacePicker.ts @@ -7,10 +7,12 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { localize } from '../../../../nls.js'; import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js'; import { ActionListItemKind, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js'; +import { IMenuService } from '../../../../platform/actions/common/actions.js'; import { IRemoteAgentHostService } from '../../../../platform/agentHost/common/remoteAgentHostService.js'; import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { IStorageService } from '../../../../platform/storage/common/storage.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; @@ -48,6 +50,8 @@ export class ScopedWorkspacePicker extends WorkspacePicker { @IConfigurationService configurationService: IConfigurationService, @ICommandService commandService: ICommandService, @IWorkspacesService workspacesService: IWorkspacesService, + @IMenuService menuService: IMenuService, + @IContextKeyService contextKeyService: IContextKeyService, @IAgentHostFilterService private readonly _agentHostFilterService: IAgentHostFilterService, ) { super( @@ -64,6 +68,8 @@ export class ScopedWorkspacePicker extends WorkspacePicker { configurationService, commandService, workspacesService, + menuService, + contextKeyService, ); // When the scoped host changes, if the current selection no longer diff --git a/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts b/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts index 3db12e1538cae..92a348ef5d37b 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts @@ -8,19 +8,19 @@ import * as touch from '../../../../base/browser/touch.js'; import { IAction, SubmenuAction, toAction } from '../../../../base/common/actions.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { URI, UriComponents } from '../../../../base/common/uri.js'; import { basename } from '../../../../base/common/resources.js'; -import { isNative } from '../../../../base/common/platform.js'; import { localize } from '../../../../nls.js'; import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js'; import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js'; -import { IRemoteAgentHostService, RemoteAgentHostConnectionStatus, RemoteAgentHostsEnabledSettingId } from '../../../../platform/agentHost/common/remoteAgentHostService.js'; +import { IMenuService, MenuItemAction } from '../../../../platform/actions/common/actions.js'; +import { IRemoteAgentHostService, RemoteAgentHostConnectionStatus } from '../../../../platform/agentHost/common/remoteAgentHostService.js'; import { TUNNEL_ADDRESS_PREFIX } from '../../../../platform/agentHost/common/tunnelAgentHost.js'; import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IPreferencesService } from '../../../../workbench/services/preferences/common/preferences.js'; import { IOutputService } from '../../../../workbench/services/output/common/output.js'; import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; @@ -35,6 +35,7 @@ import { ISessionsManagementService } from '../../../services/sessions/common/se import { IAgentHostSessionsProvider, isAgentHostProvider } from '../../../common/agentHostSessionsProvider.js'; import { COPILOT_PROVIDER_ID } from '../../copilotChatSessions/browser/copilotChatSessionsProvider.js'; import { IWorkspacesService, isRecentFolder } from '../../../../platform/workspaces/common/workspaces.js'; +import { Menus } from '../../../browser/menus.js'; const LEGACY_STORAGE_KEY_RECENT_PROJECTS = 'sessions.recentlyPickedProjects'; const STORAGE_KEY_RECENT_WORKSPACES = 'sessions.recentlyPickedWorkspaces'; @@ -66,8 +67,6 @@ export interface IWorkspacePickerItem { readonly selection?: IWorkspaceSelection; readonly browseActionIndex?: number; readonly checked?: boolean; - /** Remote provider reference for gear menu actions. */ - readonly remoteProvider?: IAgentHostSessionsProvider; /** Command to execute when this item is selected. */ readonly commandId?: string; } @@ -109,9 +108,11 @@ export class WorkspacePicker extends Disposable { @IClipboardService private readonly clipboardService: IClipboardService, @IPreferencesService private readonly preferencesService: IPreferencesService, @IOutputService private readonly outputService: IOutputService, - @IConfigurationService private readonly configurationService: IConfigurationService, + @IConfigurationService _configurationService: IConfigurationService, @ICommandService private readonly commandService: ICommandService, @IWorkspacesService private readonly workspacesService: IWorkspacesService, + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(); @@ -209,14 +210,7 @@ export class WorkspacePicker extends Disposable { // Workspace belongs to an unavailable remote — ignore selection return; } - if (item.remoteProvider && item.browseActionIndex === undefined) { - if (!item.remoteProvider.remoteAddress?.startsWith(TUNNEL_ADDRESS_PREFIX)) { - // Disconnected SSH host — show options menu after widget hides. - // (Disconnected tunnels are rendered as disabled with a - // refresh toolbar action, so onSelect doesn't fire for them.) - this._showRemoteHostOptionsDelayed(item.remoteProvider); - } - } else if (item.browseActionIndex !== undefined) { + if (item.browseActionIndex !== undefined) { this._executeBrowseAction(item.browseActionIndex); } else if (item.selection) { this._selectProject(item.selection); @@ -453,107 +447,78 @@ export class WorkspacePicker extends Disposable { }); } - if (items.length > 0 && items[items.length - 1].kind !== ActionListItemKind.Separator && remoteProviders.length) { - items.push({ kind: ActionListItemKind.Separator, label: '' }); - } + // "Manage" submenu: dynamic remote provider entries + menu-contributed actions + // Dynamic remote provider entries + const remoteProviderActions: IAction[] = []; for (const provider of remoteProviders) { const status = provider.connectionStatus!.get(); - const isConnected = status === RemoteAgentHostConnectionStatus.Connected; - const providerBrowseIndex = allBrowseActions.findIndex(a => a.providerId === provider.id); const isTunnel = provider.remoteAddress?.startsWith(TUNNEL_ADDRESS_PREFIX); - - const toolbarActions: IAction[] = []; - - if (isTunnel) { - // Offline/connecting tunnels: surface a refresh button that - // attempts to (re)connect in case the cached status is stale. - if (!isConnected && providerBrowseIndex >= 0) { - const browseIndex = providerBrowseIndex; - toolbarActions.push(toAction({ - id: `workspacePicker.remote.refresh.${provider.id}`, - label: localize('workspacePicker.refreshTunnel', "Attempt to Connect"), - class: ThemeIcon.asClassName(Codicon.refresh), - run: () => { - this.actionWidgetService.hide(); - this._executeBrowseAction(browseIndex); - }, - })); - } - } else { - // Gear menu only for SSH hosts, not tunnel providers - toolbarActions.push(toAction({ - id: `workspacePicker.remote.gear.${provider.id}`, - label: localize('workspacePicker.remoteOptions', "Options"), - class: ThemeIcon.asClassName(Codicon.gear), - run: () => { - this.actionWidgetService.hide(); - this._showRemoteHostOptionsDelayed(provider); - }, - })); - } - - items.push({ - kind: ActionListItemKind.Action, + const action = toAction({ + id: `workspacePicker.remote.${provider.id}`, label: provider.label, - description: this._getStatusDescription(status), - hover: { content: this._getStatusHover(status, provider.remoteAddress) }, - group: { title: '', icon: isTunnel ? Codicon.cloud : Codicon.remote }, - disabled: !isConnected, - item: { - browseActionIndex: isConnected && providerBrowseIndex >= 0 ? providerBrowseIndex : undefined, - remoteProvider: provider, + tooltip: this._getStatusLabel(status), + enabled: true, + run: () => { + this.actionWidgetService.hide(); + this._showRemoteHostOptionsDelayed(provider); }, - toolbarActions, }); + const extended = action as IAction & { icon?: ThemeIcon; hoverContent?: string }; + extended.icon = isTunnel ? Codicon.cloud : Codicon.remote; + extended.hoverContent = this._getStatusHover(status, provider.remoteAddress); + remoteProviderActions.push(action); + } + + // Menu-contributed actions (e.g. Tunnels..., SSH...) + const menuContributedActions: IAction[] = []; + const menuActions = this.menuService.getMenuActions(Menus.SessionWorkspaceManage, this.contextKeyService, { renderShortTitle: true }); + for (const [, actions] of menuActions) { + for (const menuAction of actions) { + if (menuAction instanceof MenuItemAction) { + const icon = ThemeIcon.isThemeIcon(menuAction.item.icon) ? menuAction.item.icon : undefined; + menuContributedActions.push(Object.assign(menuAction, { icon })); + } + } } - // "Tunnels..." and "SSH..." entries — shown when remote agent hosts are enabled - if (this.configurationService.getValue(RemoteAgentHostsEnabledSettingId)) { + // Build submenu groups — each SubmenuAction becomes a visual group with + // automatic separators between them. + const manageSubmenuActions: SubmenuAction[] = []; + if (remoteProviderActions.length > 0) { + manageSubmenuActions.push(new SubmenuAction('workspacePicker.manage.remotes', '', remoteProviderActions)); + } + if (menuContributedActions.length > 0) { + manageSubmenuActions.push(new SubmenuAction('workspacePicker.manage.menu', '', menuContributedActions)); + } + + if (manageSubmenuActions.length > 0) { if (items.length > 0 && items[items.length - 1].kind !== ActionListItemKind.Separator) { items.push({ kind: ActionListItemKind.Separator, label: '' }); } items.push({ kind: ActionListItemKind.Action, - label: localize('workspacePicker.tunnels', "Tunnels..."), - group: { title: '', icon: Codicon.cloud }, - item: { commandId: 'workbench.action.sessions.connectViaTunnel' }, + label: localize('workspacePicker.manage', "Manage..."), + group: { title: '', icon: Codicon.settingsGear }, + item: {}, + submenuActions: manageSubmenuActions, }); - if (isNative) { - items.push({ - kind: ActionListItemKind.Action, - label: localize('workspacePicker.ssh', "SSH..."), - group: { title: '', icon: Codicon.remote }, - item: { commandId: 'workbench.action.sessions.connectViaSSH' }, - }); - } } return items; } - /** - * Returns a short status indicator with a colored circle icon for the description field. - */ - private _getStatusDescription(status: RemoteAgentHostConnectionStatus): MarkdownString { - const md = new MarkdownString(undefined, { supportThemeIcons: true }); + private _getStatusLabel(status: RemoteAgentHostConnectionStatus): string { switch (status) { case RemoteAgentHostConnectionStatus.Connected: - md.appendText(localize('workspacePicker.statusOnline', "Online")); - break; + return localize('workspacePicker.statusOnline', "Online"); case RemoteAgentHostConnectionStatus.Connecting: - md.appendText(localize('workspacePicker.statusConnecting', "Connecting")); - break; + return localize('workspacePicker.statusConnecting', "Connecting"); case RemoteAgentHostConnectionStatus.Disconnected: - md.appendText(localize('workspacePicker.statusOffline', "Offline")); - break; + return localize('workspacePicker.statusOffline', "Offline"); } - return md; } - /** - * Returns detailed hover text for a remote host's connection status. - */ private _getStatusHover(status: RemoteAgentHostConnectionStatus, address?: string): string { switch (status) { case RemoteAgentHostConnectionStatus.Connected: @@ -566,8 +531,8 @@ export class WorkspacePicker extends Disposable { : localize('workspacePicker.hoverConnecting', "Attempting to connect to remote agent host..."); case RemoteAgentHostConnectionStatus.Disconnected: return address - ? localize('workspacePicker.hoverDisconnectedAddr', "Remote agent host is disconnected. Click the gear icon for options.\n\nAddress: {0}", address) - : localize('workspacePicker.hoverDisconnected', "Remote agent host is disconnected. Click the gear icon for options."); + ? localize('workspacePicker.hoverDisconnectedAddr', "Remote agent host is disconnected.\n\nAddress: {0}", address) + : localize('workspacePicker.hoverDisconnected', "Remote agent host is disconnected."); } } diff --git a/src/vs/sessions/contrib/github/browser/models/githubPullRequestCIModel.ts b/src/vs/sessions/contrib/github/browser/models/githubPullRequestCIModel.ts index 3f0cec668525e..1c14f56eb151b 100644 --- a/src/vs/sessions/contrib/github/browser/models/githubPullRequestCIModel.ts +++ b/src/vs/sessions/contrib/github/browser/models/githubPullRequestCIModel.ts @@ -25,7 +25,10 @@ export class GitHubPullRequestCIModel extends Disposable { private readonly _overallStatus = observableValue(this, GitHubCIOverallStatus.Neutral); readonly overallStatus: IObservable = this._overallStatus; + private _pollingClients = 0; + private _refreshPromise: Promise | undefined; private readonly _pollScheduler: RunOnceScheduler; + private _disposed = false; constructor( @@ -43,7 +46,19 @@ export class GitHubPullRequestCIModel extends Disposable { /** * Refresh all CI check data. */ - async refresh(): Promise { + refresh(): Promise { + if (!this._refreshPromise) { + this._refreshPromise = this._refresh().finally(() => { + if (this._refreshPromise) { + this._refreshPromise = undefined; + } + }); + } + + return this._refreshPromise; + } + + private async _refresh(): Promise { try { const checks = await this._fetcher.getCheckRuns(this.owner, this.repo, this.headRef); this._checks.set(checks, undefined); @@ -78,27 +93,43 @@ export class GitHubPullRequestCIModel extends Disposable { * Start periodic polling. Each cycle refreshes CI check data. */ startPolling(intervalMs: number = DEFAULT_POLL_INTERVAL_MS): void { - this._pollScheduler.cancel(); - this._pollScheduler.schedule(intervalMs); + this._pollScheduler.delay = intervalMs; + + this._pollingClients++; + if (this._pollingClients === 1) { + this._pollScheduler.cancel(); + this._pollScheduler.schedule(); + } } /** * Stop periodic polling. */ stopPolling(): void { - this._pollScheduler.cancel(); + if (this._pollingClients === 0) { + return; + } + + this._pollingClients--; + if (this._pollingClients === 0) { + this._pollScheduler.cancel(); + } } private async _poll(): Promise { await this.refresh(); + // Re-schedule if not disposed (RunOnceScheduler is one-shot) - if (!this._disposed) { + if (!this._disposed && this._pollingClients > 0) { this._pollScheduler.schedule(); } } override dispose(): void { this._disposed = true; + this._pollingClients = 0; + this._refreshPromise = undefined; + super.dispose(); } } diff --git a/src/vs/sessions/contrib/github/browser/models/githubPullRequestModel.ts b/src/vs/sessions/contrib/github/browser/models/githubPullRequestModel.ts index 0af71dfbbe929..510e4811ab6b6 100644 --- a/src/vs/sessions/contrib/github/browser/models/githubPullRequestModel.ts +++ b/src/vs/sessions/contrib/github/browser/models/githubPullRequestModel.ts @@ -28,7 +28,10 @@ export class GitHubPullRequestModel extends Disposable { private readonly _reviewThreads = observableValue(this, []); readonly reviewThreads: IObservable = this._reviewThreads; + private _pollingClients = 0; + private _refreshPromise: Promise | undefined; private readonly _pollScheduler: RunOnceScheduler; + private _disposed = false; constructor( @@ -46,8 +49,16 @@ export class GitHubPullRequestModel extends Disposable { /** * Refresh all PR data: pull request info, mergeability, and review threads. */ - async refresh(): Promise { - await this._refresh(); + refresh(): Promise { + if (!this._refreshPromise) { + this._refreshPromise = this._refresh().finally(() => { + if (this._refreshPromise) { + this._refreshPromise = undefined; + } + }); + } + + return this._refreshPromise; } /** @@ -80,23 +91,34 @@ export class GitHubPullRequestModel extends Disposable { * Start periodic polling. Each cycle refreshes all PR data. */ startPolling(intervalMs: number = DEFAULT_POLL_INTERVAL_MS): void { - this._pollScheduler.cancel(); - this._pollScheduler.schedule(intervalMs); + this._pollScheduler.delay = intervalMs; + + this._pollingClients++; + if (this._pollingClients === 1) { + this._pollScheduler.cancel(); + this._pollScheduler.schedule(); + } } /** * Stop periodic polling. */ stopPolling(): void { - this._pollScheduler.cancel(); + if (this._pollingClients === 0) { + return; + } + + this._pollingClients--; + if (this._pollingClients === 0) { + this._pollScheduler.cancel(); + } } private async _poll(): Promise { await this.refresh(); - // Re-schedule for next poll cycle - // as RunOnceScheduler is one-shot - if (!this._disposed) { + // Re-schedule if not disposed (RunOnceScheduler is one-shot) + if (!this._disposed && this._pollingClients > 0) { this._pollScheduler.schedule(); } } @@ -117,6 +139,9 @@ export class GitHubPullRequestModel extends Disposable { override dispose(): void { this._disposed = true; + this._pollingClients = 0; + this._refreshPromise = undefined; + super.dispose(); } } diff --git a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostActions.ts b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostActions.ts index 1ea5543af0345..64881379642de 100644 --- a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostActions.ts +++ b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostActions.ts @@ -5,6 +5,7 @@ import { localize, localize2 } from '../../../../nls.js'; import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { Codicon } from '../../../../base/common/codicons.js'; import { IRemoteAgentHostService, parseRemoteAgentHostInput, RemoteAgentHostEntryType, RemoteAgentHostInputValidationError, RemoteAgentHostsEnabledSettingId } from '../../../../platform/agentHost/common/remoteAgentHostService.js'; import { ISSHRemoteAgentHostService, SSHAuthMethod, type ISSHAgentHostConfig, type ISSHAgentHostConnection, type ISSHResolvedConfig } from '../../../../platform/agentHost/common/sshRemoteAgentHost.js'; import { ITunnelAgentHostService, TUNNEL_ADDRESS_PREFIX, type ITunnelInfo } from '../../../../platform/agentHost/common/tunnelAgentHost.js'; @@ -16,6 +17,7 @@ import { IViewsService } from '../../../../workbench/services/views/common/views import { IAuthenticationService } from '../../../../workbench/services/authentication/common/authentication.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { SessionsCategories } from '../../../common/categories.js'; +import { Menus } from '../../../browser/menus.js'; import { NewChatViewPane, SessionsViewId } from '../../chat/browser/newChatViewPane.js'; import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js'; @@ -468,9 +470,15 @@ registerAction2(class extends Action2 { super({ id: 'workbench.action.sessions.connectViaSSH', title: localize2('connectViaSSH', "Connect to Remote Agent Host via SSH"), + shortTitle: localize2('connectViaSSHShort', "SSH..."), category: SessionsCategories.Sessions, f1: true, + icon: Codicon.remote, precondition: ContextKeyExpr.equals(`config.${RemoteAgentHostsEnabledSettingId}`, true), + menu: { + id: Menus.SessionWorkspaceManage, + order: 20, + }, }); } @@ -647,9 +655,15 @@ registerAction2(class extends Action2 { super({ id: 'workbench.action.sessions.connectViaTunnel', title: localize2('connectViaTunnel', "Connect to Remote Agent Host via Dev Tunnel"), + shortTitle: localize2('connectViaTunnelShort', "Tunnels..."), category: SessionsCategories.Sessions, f1: true, + icon: Codicon.cloud, precondition: ContextKeyExpr.equals(`config.${RemoteAgentHostsEnabledSettingId}`, true), + menu: { + id: Menus.SessionWorkspaceManage, + order: 10, + }, }); } diff --git a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts index 58ef0bf07a00b..a6ef3df422be7 100644 --- a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts +++ b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostCustomizationHarness.ts @@ -90,6 +90,8 @@ export class RemoteAgentCustomizationItemProvider extends Disposable implements status: toStatusString(sc.status), statusMessage: sc.statusMessage, enabled: sc.enabled, + extensionId: undefined, + pluginUri: undefined })); } @@ -99,6 +101,8 @@ export class RemoteAgentCustomizationItemProvider extends Disposable implements type: 'plugin', name: ref.displayName, description: ref.description, + extensionId: undefined, + pluginUri: undefined })); } } diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index 0411c189ded57..351500b090211 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -94,12 +94,21 @@ import '../workbench/services/browserView/electron-browser/playwrightWorkbenchSe import '../workbench/services/process/electron-browser/processService.js'; import '../workbench/services/power/electron-browser/powerService.js'; -import { registerSingleton } from '../platform/instantiation/common/extensions.js'; +import { ILocalGitService } from '../platform/git/common/localGitService.js'; +import { InstantiationType, registerSingleton } from '../platform/instantiation/common/extensions.js'; +import { registerSharedProcessRemoteService } from '../platform/ipc/electron-browser/services.js'; +import { IPluginGitService } from '../workbench/contrib/chat/common/plugins/pluginGitService.js'; +import { NativePluginGitCommandService } from '../workbench/contrib/chat/electron-browser/pluginGitCommandService.js'; import { IUserDataInitializationService, UserDataInitializationService } from '../workbench/services/userData/browser/userDataInit.js'; import { SyncDescriptor } from '../platform/instantiation/common/descriptors.js'; registerSingleton(IUserDataInitializationService, new SyncDescriptor(UserDataInitializationService, [[]], true)); +// Override the browser PluginGitCommandService with the native one that always +// runs git locally via the shared process. +registerSingleton(IPluginGitService, NativePluginGitCommandService, InstantiationType.Delayed); +registerSharedProcessRemoteService(ILocalGitService, 'localGit'); + //#endregion diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index fed4a81015adb..db1ab19404444 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -746,6 +746,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA groupKey: item.groupKey, badge: item.badge, badgeTooltip: item.badgeTooltip, + extensionId: undefined, + pluginUri: undefined })); }, }; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index f03878df02b2a..2ba3793dc283a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1738,6 +1738,7 @@ export interface IChatSessionCustomizationItemDto { readonly description?: string; readonly groupKey?: string; readonly badge?: string; + readonly badgeTooltip?: string; } export interface IChatParticipantMetadata { diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 58b98ea5a42df..0b58d688ac7e4 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -830,8 +830,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS description: item.description, groupKey: item.groupKey, badge: item.badge, - badgeTooltip: item.badgeTooltip, - })); + badgeTooltip: item.badgeTooltip + } satisfies IChatSessionCustomizationItemDto)); } catch (err) { return undefined; } diff --git a/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserToolNonAgentic.ts b/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserToolNonAgentic.ts index 149fdd44c796a..c280c2955482a 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserToolNonAgentic.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/tools/openBrowserToolNonAgentic.ts @@ -63,7 +63,7 @@ export class OpenBrowserToolNonAgentic implements IToolImpl { logBrowserOpen(this.telemetryService, 'chatTool'); const browserUri = BrowserViewUri.forId(generateUuid()); - await this.editorService.openEditor({ resource: browserUri, options: { pinned: true, viewState: { url: params.url } } }); + await this.editorService.openEditor({ resource: browserUri, options: { pinned: true, preserveFocus: true, viewState: { url: params.url } } }); return { content: [{ diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 1463d518357d3..d5b7479cbf6c0 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -29,7 +29,7 @@ import { IEditorService } from '../../../../services/editor/common/editorService import { accessibleViewInCodeBlock } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js'; import { EditDeltaInfo } from '../../../../../editor/common/textModelEditSource.js'; -import { reviewEdits } from '../../../inlineChat/browser/inlineChatController.js'; +import { reviewEdits } from './reviewEdits.js'; import { ITerminalEditorService, ITerminalGroupService, ITerminalService } from '../../../terminal/browser/terminal.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { ChatCopyKind, IChatService } from '../../common/chatService/chatService.js'; diff --git a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index df32c865dff3b..347b30e943a7a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -29,7 +29,7 @@ import { IQuickInputService } from '../../../../../platform/quickinput/common/qu import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js'; -import { reviewEdits, reviewNotebookEdits } from '../../../inlineChat/browser/inlineChatController.js'; +import { reviewEdits, reviewNotebookEdits } from './reviewEdits.js'; import { insertCell } from '../../../notebook/browser/controller/cellOperations.js'; import { IActiveNotebookEditor, INotebookEditor } from '../../../notebook/browser/notebookBrowser.js'; import { CellKind, ICellEditOperation, NOTEBOOK_EDITOR_ID } from '../../../notebook/common/notebookCommon.js'; diff --git a/src/vs/workbench/contrib/chat/browser/actions/reviewEdits.ts b/src/vs/workbench/contrib/chat/browser/actions/reviewEdits.ts new file mode 100644 index 0000000000000..5fdad2d2915ba --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/actions/reviewEdits.ts @@ -0,0 +1,146 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { raceCancellation } from '../../../../../base/common/async.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { derived, waitForState } from '../../../../../base/common/observable.js'; +import { assertType } from '../../../../../base/common/types.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; +import { TextEdit } from '../../../../../editor/common/languages.js'; +import { EditSuggestionId } from '../../../../../editor/common/textModelEditSource.js'; +import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IChatService } from '../../common/chatService/chatService.js'; +import { ChatAgentLocation } from '../../common/constants.js'; +import { ModifiedFileEntryState } from '../../common/editing/chatEditingService.js'; +import { ChatModel } from '../../common/model/chatModel.js'; +import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js'; +import { INotebookService } from '../../../notebook/common/notebookService.js'; + + +export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEditor, stream: AsyncIterable, token: CancellationToken, applyCodeBlockSuggestionId: EditSuggestionId | undefined): Promise { + if (!editor.hasModel()) { + return false; + } + + const chatService = accessor.get(IChatService); + const uri = editor.getModel().uri; + const chatModelRef = chatService.startNewLocalSession(ChatAgentLocation.EditorInline); + const chatModel = chatModelRef.object as ChatModel; + + chatModel.startEditingSession(true); + + const store = new DisposableStore(); + store.add(chatModelRef); + + // STREAM + const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0, { + kind: undefined, + modeId: 'applyCodeBlock', + modeInstructions: undefined, + isBuiltin: true, + applyCodeBlockSuggestionId, + }); + assertType(chatRequest.response); + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false }); + for await (const chunk of stream) { + + if (token.isCancellationRequested) { + chatRequest.response.cancel(); + break; + } + + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: chunk, done: false }); + } + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); + + if (!token.isCancellationRequested) { + chatRequest.response.complete(); + } + + const isSettled = derived(r => { + const entry = chatModel.editingSession?.readEntry(uri, r); + if (!entry) { + return false; + } + const state = entry.state.read(r); + return state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected; + }); + const whenDecided = waitForState(isSettled, Boolean); + await raceCancellation(whenDecided, token); + store.dispose(); + return true; +} + +export async function reviewNotebookEdits(accessor: ServicesAccessor, uri: URI, stream: AsyncIterable<[URI, TextEdit[]] | ICellEditOperation[]>, token: CancellationToken): Promise { + + const chatService = accessor.get(IChatService); + const notebookService = accessor.get(INotebookService); + const isNotebook = notebookService.hasSupportedNotebooks(uri); + const chatModelRef = chatService.startNewLocalSession(ChatAgentLocation.EditorInline); + const chatModel = chatModelRef.object as ChatModel; + + chatModel.startEditingSession(true); + + const store = new DisposableStore(); + store.add(chatModelRef); + + // STREAM + const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0); + assertType(chatRequest.response); + if (isNotebook) { + chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: [], done: false }); + } else { + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false }); + } + for await (const chunk of stream) { + + if (token.isCancellationRequested) { + chatRequest.response.cancel(); + break; + } + if (chunk.every(isCellEditOperation)) { + chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: chunk, done: false }); + } else { + chatRequest.response.updateContent({ kind: 'textEdit', uri: chunk[0], edits: chunk[1], done: false }); + } + } + if (isNotebook) { + chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: [], done: true }); + } else { + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); + } + + if (!token.isCancellationRequested) { + chatRequest.response.complete(); + } + + const isSettled = derived(r => { + const entry = chatModel.editingSession?.readEntry(uri, r); + if (!entry) { + return false; + } + const state = entry.state.read(r); + return state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected; + }); + + const whenDecided = waitForState(isSettled, Boolean); + + await raceCancellation(whenDecided, token); + + store.dispose(); + + return true; +} +function isCellEditOperation(edit: URI | TextEdit[] | ICellEditOperation): edit is ICellEditOperation { + if (URI.isUri(edit)) { + return false; + } + if (Array.isArray(edit)) { + return false; + } + return true; +} diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemSource.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemSource.ts index f394aa0176355..2d643c9065853 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemSource.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemSource.ts @@ -156,6 +156,8 @@ export async function expandHookFileItems( enabled: item.enabled, groupKey: item.groupKey, storage: item.storage, + extensionId: item.extensionId, + pluginUri: item.pluginUri }); } } @@ -449,6 +451,8 @@ export class ProviderCustomizationItemSource implements IAICustomizationItemSour enabled: !disabledPromptFiles.has(p.uri), badge: uiTooltip ? uiIntegrationBadge : undefined, badgeTooltip: uiTooltip, + extensionId: undefined, + pluginUri: undefined }; appended.push(this.itemNormalizer.normalizeItem(builtinItem, promptType, uriUseCounts)); } @@ -484,6 +488,8 @@ export class ProviderCustomizationItemSource implements IAICustomizationItemSour name: getFriendlyName(basename(file.uri)), groupKey: 'sync-local', enabled: true, + extensionId: undefined, + pluginUri: undefined })); return this.itemNormalizer.normalizeItems(providerItems, promptType) diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePagePromptLaunchers.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePagePromptLaunchers.ts index 685f9e226f786..bf7bfb1909455 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePagePromptLaunchers.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePagePromptLaunchers.ts @@ -5,6 +5,8 @@ import './media/aiCustomizationWelcomePromptLaunchers.css'; import * as DOM from '../../../../../base/browser/dom.js'; +import { DomScrollableElement } from '../../../../../base/browser/ui/scrollbar/scrollableElement.js'; +import { ScrollbarVisibility } from '../../../../../base/common/scrollable.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { localize } from '../../../../../nls.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; @@ -33,6 +35,7 @@ export class PromptLaunchersAICustomizationWelcomePage extends Disposable implem private readonly cardDisposables = this._register(new DisposableStore()); readonly container: HTMLElement; + private readonly scrollable: DomScrollableElement; private cardsContainer: HTMLElement | undefined; private inputElement: HTMLInputElement | undefined; @@ -93,7 +96,21 @@ export class PromptLaunchersAICustomizationWelcomePage extends Disposable implem ) { super(); - this.container = DOM.append(parent, $('.welcome-prompts-content-container')); + this.container = $('.welcome-prompts-content-container'); + this.scrollable = this._register(new DomScrollableElement(this.container, { + horizontal: ScrollbarVisibility.Hidden, + vertical: ScrollbarVisibility.Auto, + useShadows: false, + })); + const scrollableNode = this.scrollable.getDomNode(); + scrollableNode.classList.add('welcome-prompts-scrollable'); + parent.appendChild(scrollableNode); + + // Re-scan whenever the wrapper changes size so the scrollbar reflects + // the current overflow state. rebuildCards() scans after content changes. + const resizeObserver = this._register(new DOM.DisposableResizeObserver(() => this.scrollable.scanDomNode())); + this._register(resizeObserver.observe(scrollableNode)); + const welcomeInner = DOM.append(this.container, $('.welcome-prompts-inner')); const heading = DOM.append(welcomeInner, $('h2.welcome-prompts-heading')); @@ -256,6 +273,9 @@ export class PromptLaunchersAICustomizationWelcomePage extends Disposable implem } })); } + + // Content changed — recompute scroll dimensions. + this.scrollable.scanDomNode(); } focus(): void { diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css index b1943a20d8661..addcea886cfe4 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css @@ -208,10 +208,9 @@ align-items: center; gap: 6px; width: 100%; - padding: 5px 8px; + padding: 3px 6px; border: 1px solid var(--vscode-dropdown-border, transparent); - border-radius: 4px; - background: var(--vscode-dropdown-background); + border-radius: 6px; color: var(--vscode-dropdown-foreground); cursor: pointer; font-size: 12px; @@ -220,7 +219,6 @@ .ai-customization-management-editor .harness-dropdown-button:hover { background: var(--vscode-dropdown-background); - border-color: var(--vscode-focusBorder); } .ai-customization-management-editor .harness-dropdown-button:focus-visible { diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationWelcomePromptLaunchers.css b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationWelcomePromptLaunchers.css index e076c82edcae5..ba32a4d4a39f9 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationWelcomePromptLaunchers.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationWelcomePromptLaunchers.css @@ -3,11 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.ai-customization-management-editor .welcome-prompts-content-container { +/* The DomScrollableElement wrapper fills the welcome page host */ +.ai-customization-management-editor .welcome-prompts-scrollable { + position: relative; + width: 100%; height: 100%; - overflow-y: auto; +} + +/* The inner element is the one DomScrollableElement scrolls. + * Constrain it so clientHeight < scrollHeight when content overflows. */ +.ai-customization-management-editor .welcome-prompts-content-container { + position: absolute; + inset: 0; padding: 24px; box-sizing: border-box; + overflow: hidden; } .ai-customization-management-editor .welcome-prompts-inner { diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/promptsServiceCustomizationItemProvider.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/promptsServiceCustomizationItemProvider.ts index 7e710a7c5232b..7f8c78a4efe9b 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/promptsServiceCustomizationItemProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/promptsServiceCustomizationItemProvider.ts @@ -75,6 +75,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt description: agent.description, storage: agent.source.storage, enabled: !disabledUris.has(agent.uri), + extensionId: agent.source.storage === PromptsStorage.extension ? agent.source.extensionId.value : undefined, + pluginUri: agent.source.storage === PromptsStorage.plugin ? agent.source.pluginUri : undefined }); if (agent.source.storage === PromptsStorage.extension && !extensionInfoByUri.has(agent.uri)) { extensionInfoByUri.set(agent.uri, { id: agent.source.extensionId }); @@ -104,6 +106,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt enabled: true, badge: uiTooltip ? localize('uiIntegrationBadge', "UI Integration") : undefined, badgeTooltip: uiTooltip, + extensionId: skill.extension?.identifier.value, + pluginUri: skill.pluginUri }); } if (disabledUris.size > 0) { @@ -121,6 +125,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt enabled: false, badge: uiTooltip ? localize('uiIntegrationBadge', "UI Integration") : undefined, badgeTooltip: uiTooltip, + extensionId: file.extension?.identifier.value, + pluginUri: file.pluginUri }); } } @@ -138,6 +144,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt description: command.description, storage: command.storage, enabled: !disabledUris.has(command.uri), + extensionId: command.extension?.identifier.value, + pluginUri: command.pluginUri }); if (command.extension) { extensionInfoByUri.set(command.uri, { id: command.extension.identifier, displayName: command.extension.displayName }); @@ -166,6 +174,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt name: f.name || getFriendlyName(basename(f.uri)), storage: f.storage, enabled: !disabledUris.has(f.uri), + extensionId: f.extension?.identifier.value, + pluginUri: f.pluginUri }); } @@ -193,6 +203,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt storage: agent.source.storage, groupKey: 'agents', enabled: !disabledUris.has(agent.uri), + extensionId: agent.source.storage === PromptsStorage.extension ? agent.source.extensionId.value : undefined, + pluginUri: agent.source.storage === PromptsStorage.plugin ? agent.source.pluginUri : undefined }); } } @@ -219,10 +231,12 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt storage, groupKey: 'agent-instructions', enabled: !disabledUris.has(file.uri), + extensionId: undefined, + pluginUri: undefined }); } - for (const { uri, pattern, name, description, storage } of instructionFiles) { + for (const { uri, pattern, name, description, storage, extension, pluginUri } of instructionFiles) { if (agentInstructionUris.has(uri)) { continue; } @@ -246,6 +260,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt storage, groupKey: 'context-instructions', enabled: !disabledUris.has(uri), + extensionId: extension?.identifier.value, + pluginUri }); } else { items.push({ @@ -256,6 +272,8 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt storage, groupKey: 'on-demand-instructions', enabled: !disabledUris.has(uri), + extensionId: extension?.identifier.value, + pluginUri }); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts index f1064adb378ad..35ddb2477e172 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts @@ -29,6 +29,7 @@ import { renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels. import { ThemeIcon } from '../../../../../base/common/themables.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { IWorkbenchEnvironmentService } from '../../../../services/environment/common/environmentService.js'; +import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; export class ChatEditingAcceptRejectActionViewItem extends ActionViewItem { @@ -319,6 +320,11 @@ class ChatEditingOverlayController { activeEditorSignal.read(r); // signal const editor = group.activeEditorPane; + + if (!getCodeEditor(editor?.getControl())) { + return undefined; + } + const uri = EditorResourceAccessor.getOriginalUri(editor?.input, { supportSideBySide: SideBySideEditor.PRIMARY }); return uri; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts index ba31aec8a0f89..aeb355759dfc0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts @@ -237,10 +237,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic const enum K { Stream, Workspace } const editsSeen: ({ kind: K.Stream; seen: number; stream: IStreamingEdits } | { kind: K.Workspace })[] = []; - let editorDidChange = false; - const editorListener = Event.once(this._editorService.onDidActiveEditorChange)(() => { - editorDidChange = true; - }); + const initialActiveEditor = this._editorService.activeEditorPane?.input; const editorOpenPromises = new ResourceMap>(); const openChatEditedFiles = this._configurationService.getValue('accessibility.openChatEditedFiles'); @@ -252,6 +249,8 @@ export class ChatEditingService extends Disposable implements IChatEditingServic editorOpenPromises.set(uri, (async () => { if (this.notebookService.getNotebookTextModel(uri) || uri.scheme === Schemas.untitled || await this._fileService.exists(uri).catch(() => false)) { const activeUri = this._editorService.activeEditorPane?.input.resource; + const currentActiveEditor = this._editorService.activeEditorPane?.input; + const editorDidChange = initialActiveEditor && currentActiveEditor ? !initialActiveEditor.matches(currentActiveEditor) : initialActiveEditor !== currentActiveEditor; const inactive = editorDidChange || this._editorService.activeEditorPane?.input instanceof ChatEditorInput && isEqual(this._editorService.activeEditorPane.input.sessionResource, session.chatSessionResource) || Boolean(activeUri && session.entries.get().find(entry => isEqual(activeUri, entry.modifiedURI))); @@ -270,7 +269,6 @@ export class ChatEditingService extends Disposable implements IChatEditingServic editsSeen.length = 0; editorOpenPromises.clear(); - editorListener.dispose(); }; const handleResponseParts = async () => { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index a38fa48c0a2d3..68153748c4027 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -3506,10 +3506,59 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge layout(width: number) { this.cachedWidth = width; this._stableInputPartWidth.set(width, undefined); + this._updateWorkingProgressAnimationDuration(width); return this._layout(width); } + /** + * Scale the working/progress border comet animation duration with + * the input width so the comet's perceived linear travel speed (the + * rate it sweeps along the perimeter in px/sec) stays roughly + * constant. A fixed cycle time made wide inputs feel sluggish, but + * an aggressive inverse curve made narrow inputs feel slow because + * their cycle was clamped while the comet had little distance to + * cover. Sub-linear scaling with width (`sqrt(width)`) plus tight + * clamps keeps both extremes looking lively. + */ + private _lastAnimDurationS: number | undefined; + private _updateWorkingProgressAnimationDuration(width: number): void { + if (!this.inputContainer) { + return; + } + // Sub-linear scaling: cycle time grows with width but tapers off + // so wide inputs still feel snappy. Tuned so ~400px → ~1.7s and + // ~1000px → ~2.3s rather than ~4s. + const MIN_DURATION_S = 1.4; + const MAX_DURATION_S = 2.5; + const safeWidth = Math.max(50, width); + const raw = 0.55 + 0.075 * Math.sqrt(safeWidth); + const duration = Math.min(MAX_DURATION_S, Math.max(MIN_DURATION_S, raw)); + + // Skip no-op updates (e.g. repeated layout calls during steady state). + if (this._lastAnimDurationS !== undefined && Math.abs(this._lastAnimDurationS - duration) < 0.05) { + return; + } + this._lastAnimDurationS = duration; + this.inputContainer.style.setProperty('--chat-input-anim-duration', `${duration.toFixed(2)}s`); + + // CSS animations capture animation-duration at start time and most + // browsers do not re-pick up values that come from a custom + // property mid-flight. If the comet is currently spinning, restart + // it on the next animation frame so style and layout changes can + // batch without forcing a synchronous reflow. Toggling the .working + // class would cancel the in-flight indicator state, so instead we + // briefly flip a marker class that the CSS uses to swap + // animation-name. + if (this.inputContainer.classList.contains('working')) { + const inputContainer = this.inputContainer; + inputContainer.classList.add('chat-input-anim-restart'); + dom.scheduleAtNextAnimationFrame(dom.getWindow(inputContainer), () => { + inputContainer.classList.remove('chat-input-anim-restart'); + }); + } + } + private get _effectiveInputEditorMaxHeight(): number { if (this._maxHeight === undefined) { return this.inputEditorMaxHeight; diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 7cf400fcbd506..dc5a29920e3e8 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -862,6 +862,11 @@ have to be updated for changes to the rules above, or to support more deeply nes width: 100%; position: relative; transition: box-shadow 350ms ease; + /* Duration of the working/progress border comet animation. Set + dynamically by `ChatInputPart#layout` to keep the comet's linear + travel speed roughly constant regardless of input width — wider + inputs would otherwise feel sluggish at a fixed duration. */ + --chat-input-anim-duration: 4s; } /* Prevent contents from covering border corners. Not applied in compact mode @@ -967,18 +972,32 @@ have to be updated for changes to the rules above, or to support more deeply nes } .monaco-workbench .interactive-session .chat-input-container.working { - border-color: transparent; + /* Keep a faint, persistent ring around the input while the comet + animates around it. This preserves a visible boundary for the + input throughout the animation (including the dim portion of the + perimeter behind the comet) and improves contrast for users who + rely on a visible focus/container outline for accessibility. */ + border-color: var(--vscode-input-border, transparent); overflow: visible; } .monaco-workbench .interactive-session .chat-input-container.working::before { opacity: 1; - animation: chat-input-working-border-spin 4s linear infinite; + animation: chat-input-working-border-spin var(--chat-input-anim-duration) linear infinite; } .monaco-workbench .interactive-session .chat-input-container.working::after { opacity: 1; - animation: chat-input-working-border-spin 4s linear infinite; + animation: chat-input-working-border-spin var(--chat-input-anim-duration) linear infinite; +} + +/* Marker class toggled briefly by `ChatInputPart#_updateWorkingProgressAnimationDuration` + to force a restart of the comet animations so a new + `--chat-input-anim-duration` takes effect mid-flight (browsers cache + animation-duration at start time when sourced from a custom property). */ +.monaco-workbench .interactive-session .chat-input-container.working.chat-input-anim-restart::before, +.monaco-workbench .interactive-session .chat-input-container.working.chat-input-anim-restart::after { + animation: none; } @media (prefers-reduced-motion: reduce) { @@ -1473,6 +1492,8 @@ have to be updated for changes to the rules above, or to support more deeply nes .chat-editor-container { padding: 0 0 0 4px; + /* enables 100cqi for placeholder truncation below */ + container-type: inline-size; } .interactive-session .interactive-input-part.compact .chat-editor-container { @@ -1484,6 +1505,23 @@ have to be updated for changes to the rules above, or to support more deeply nes color: var(--vscode-input-foreground); } +/* Truncate the chat input placeholder with an ellipsis in narrow views + instead of letting it overflow off the right edge of the input. + The placeholder is rendered as a Monaco contentText decoration with + class prefix "ced-chat-session-detail" inside the view-line spans; + style the decoration element directly so we don't pay :has() cost + across every view-line on each typed character. */ +.chat-editor-container .monaco-editor [class^="ced-chat-session-detail"] { + display: inline-block; + max-width: 100%; /* fallback for environments without container query units */ + max-width: 100cqi; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: bottom; + color: var(--vscode-input-placeholderForeground); +} + .interactive-session .chat-editor-container .monaco-editor .chat-prompt-spinner { transform-origin: 6px 6px; font-size: 12px; diff --git a/src/vs/workbench/contrib/chat/common/chatModes.ts b/src/vs/workbench/contrib/chat/common/chatModes.ts index ce81c152d2095..b510a3e306b12 100644 --- a/src/vs/workbench/contrib/chat/common/chatModes.ts +++ b/src/vs/workbench/contrib/chat/common/chatModes.ts @@ -494,7 +494,7 @@ function serializeChatModeSource(source: IAgentSource | undefined): IChatModeSou return undefined; } if (source.storage === PromptsStorage.extension) { - return { storage: PromptsStorage.extension, extensionId: source.extensionId.value, type: source.type }; + return { storage: PromptsStorage.extension, extensionId: source.extensionId.value }; } if (source.storage === PromptsStorage.plugin) { return { storage: PromptsStorage.plugin, pluginUri: source.pluginUri }; @@ -507,14 +507,7 @@ function reviveChatModeSource(data: IChatModeSourceData | undefined): IAgentSour return undefined; } if (data.storage === PromptsStorage.extension) { - // Migrate old ExtensionAgentSourceType values ('contribution'/'provider') to PromptFileSource values - let type: PromptFileSource.ExtensionContribution | PromptFileSource.ExtensionAPI; - if (data.type === 'provider' as string /* old type value */ || data.type === PromptFileSource.ExtensionAPI) { - type = PromptFileSource.ExtensionAPI; - } else { - type = PromptFileSource.ExtensionContribution; - } - return { storage: PromptsStorage.extension, extensionId: new ExtensionIdentifier(data.extensionId), type }; + return { storage: PromptsStorage.extension, extensionId: new ExtensionIdentifier(data.extensionId) }; } if (data.storage === PromptsStorage.plugin) { return { storage: PromptsStorage.plugin, pluginUri: URI.revive(data.pluginUri) }; diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index d8b405fdd712c..613f5cfeaeee7 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -7,7 +7,7 @@ import { Schemas } from '../../../../base/common/network.js'; import { IChatSessionsService } from './chatSessionsService.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { ContextKeyExpr, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { IsDevelopmentContext, IsLinuxContext } from '../../../../platform/contextkey/common/contextkeys.js'; +import { ProductQualityContext } from '../../../../platform/contextkey/common/contextkeys.js'; import { ChatEntitlementContextKeys } from '../../../services/chat/common/chatEntitlementService.js'; import { IsSessionsWindowContext } from '../../../common/contextkeys.js'; @@ -198,7 +198,7 @@ export const MANAGE_CHAT_COMMAND_ID = 'workbench.action.chat.manage'; export const OPEN_AGENTS_WINDOW_COMMAND_ID = 'workbench.action.openAgentsWindow'; export const OPEN_AGENTS_WINDOW_PRECONDITION = ContextKeyExpr.and( - ContextKeyExpr.or(IsLinuxContext.negate(), IsDevelopmentContext), + ProductQualityContext.notEqualsTo('stable'), ChatEntitlementContextKeys.Setup.hidden.negate(), ChatEntitlementContextKeys.Setup.disabledInWorkspace.negate(), IsSessionsWindowContext.negate(), diff --git a/src/vs/workbench/contrib/chat/common/customizationHarnessService.ts b/src/vs/workbench/contrib/chat/common/customizationHarnessService.ts index a7fa2f855fffb..83b2a474a7b57 100644 --- a/src/vs/workbench/contrib/chat/common/customizationHarnessService.ts +++ b/src/vs/workbench/contrib/chat/common/customizationHarnessService.ts @@ -15,9 +15,11 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta import { AICustomizationManagementSection, IStorageSourceFilter } from './aiCustomizationWorkspaceService.js'; import { PromptsType } from './promptSyntax/promptTypes.js'; import { AGENT_MD_FILENAME } from './promptSyntax/config/promptFileLocations.js'; -import { IChatPromptSlashCommand, IPromptsService, IResolvedChatPromptSlashCommand, matchesSessionType, PromptsStorage } from './promptSyntax/service/promptsService.js'; +import { IAgentSource, IChatPromptSlashCommand, ICustomAgent, IPromptsService, IResolvedChatPromptSlashCommand, matchesSessionType, PromptsStorage } from './promptSyntax/service/promptsService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { SessionType } from './chatSessionsService.js'; +import { CustomAgent } from './promptSyntax/service/promptsServiceImpl.js'; +import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; export const ICustomizationHarnessService = createDecorator('customizationHarnessService'); @@ -146,6 +148,10 @@ export interface ICustomizationItem { readonly storage?: PromptsStorage; /** Display name of the contributing extension (e.g. "GitHub Copilot Chat"). */ readonly extensionLabel?: string; + /** The extension identifier that contributed this customization, if any. */ + readonly extensionId: string | undefined; + /** The URI of the plugin that contributed this customization, if any. */ + readonly pluginUri: URI | undefined; /** Server-reported loading status for this customization. */ readonly status?: 'loading' | 'loaded' | 'degraded' | 'error'; /** Human-readable status detail (e.g. error message or warning). */ @@ -265,6 +271,11 @@ export interface ICustomizationHarnessService { */ readonly onDidChangeSlashCommands: Event<{ readonly sessionType: string }>; + /** + * Fires when one of the provided custom agents changes. + */ + readonly onDidChangeCustomAgents: Event<{ readonly sessionType: string }>; + /** * Returns the prompt and skill slash commands for the given session type. * Provider-backed harnesses contribute their own items directly; the default @@ -272,6 +283,13 @@ export interface ICustomizationHarnessService { */ getSlashCommands(sessionType: string, token: CancellationToken): Promise; + /** + * Returns the custom agents for the given session type. + * Provider-backed harnesses select items via their own provider and resolve + * details via the core prompts service. + */ + getCustomAgents(sessionType: string, token: CancellationToken): Promise; + /** * Resolves a slash command to its full metadata, including the parsed prompt file for prompt commands. * Provider-backed harnesses resolve their own items directly; the default VS Code harness falls back to the core prompts service. @@ -481,7 +499,10 @@ export class CustomizationHarnessServiceBase implements ICustomizationHarnessSer declare readonly _serviceBrand: undefined; private readonly _onDidChangeSlashCommands = new Emitter<{ readonly sessionType: string }>(); readonly onDidChangeSlashCommands = this._onDidChangeSlashCommands.event; + private readonly _onDidChangeCustomAgents = new Emitter<{ readonly sessionType: string }>(); + readonly onDidChangeCustomAgents = this._onDidChangeCustomAgents.event; private readonly _providerListeners: IDisposable[] = []; + private _isDisposed = false; private readonly _activeHarness: ISettableObservable; readonly activeHarness: IObservable; @@ -516,6 +537,9 @@ export class CustomizationHarnessServiceBase implements ICustomizationHarnessSer } private _refreshAvailableHarnesses(): void { + if (this._isDisposed) { + return; + } this._availableHarnesses.set(this._getAllHarnesses(), undefined); this._rebindProviderListeners(); } @@ -529,18 +553,22 @@ export class CustomizationHarnessServiceBase implements ICustomizationHarnessSer const provider = harness.itemProvider; if (!provider) { this._providerListeners.push(this.promptsService.onDidChangeSlashCommands(() => this._onDidChangeSlashCommands.fire({ sessionType: harness.id }))); + this._providerListeners.push(this.promptsService.onDidChangeCustomAgents(() => this._onDidChangeCustomAgents.fire({ sessionType: harness.id }))); } else { this._providerListeners.push(provider.onDidChange(() => this._onDidChangeSlashCommands.fire({ sessionType: harness.id }))); + this._providerListeners.push(provider.onDidChange(() => this._onDidChangeCustomAgents.fire({ sessionType: harness.id }))); } } } dispose(): void { + this._isDisposed = true; for (const listener of this._providerListeners) { listener.dispose(); } this._providerListeners.length = 0; this._onDidChangeSlashCommands.dispose(); + this._onDidChangeCustomAgents.dispose(); } registerExternalHarness(descriptor: IHarnessDescriptor): IDisposable { @@ -548,6 +576,9 @@ export class CustomizationHarnessServiceBase implements ICustomizationHarnessSer this._refreshAvailableHarnesses(); return { dispose: () => { + if (this._isDisposed) { + return; + } const idx = this._externalHarnesses.indexOf(descriptor); if (idx >= 0) { this._externalHarnesses.splice(idx, 1); @@ -624,6 +655,47 @@ export class CustomizationHarnessServiceBase implements ICustomizationHarnessSer return result; } + async getCustomAgents(sessionType: string, token: CancellationToken): Promise { + const harness = this.findHarnessById(sessionType); + if (!harness || !harness.itemProvider) { + const allAgents = await this.promptsService.getCustomAgents(token); + return allAgents.filter(agent => matchesSessionType(agent.sessionTypes, sessionType)); + } + + const items = await harness.itemProvider.provideChatSessionCustomizations(token); + if (!items) { + return []; + } + + const getSource = (item: ICustomizationItem): IAgentSource => { + if (item.storage === PromptsStorage.extension && item.extensionId) { + return { storage: PromptsStorage.extension, extensionId: new ExtensionIdentifier(item.extensionId) }; + } else if (item.storage === PromptsStorage.plugin && item.pluginUri) { + return { storage: PromptsStorage.plugin, pluginUri: item.pluginUri }; + } else if (item.storage === PromptsStorage.user) { + return { storage: PromptsStorage.user }; + } + return { storage: PromptsStorage.local }; + }; + + const result: ICustomAgent[] = []; + for (const item of items) { + if ((item.enabled !== false) && item.type === PromptsType.agent) { + const promptFile = await this.promptsService.parseNew(item.uri, token); + const extra = { + name: item.name, + description: item.description, + sessionTypes: [sessionType], + hooks: undefined, + source: getSource(item), + type: PromptsType.agent, + }; + result.push(CustomAgent.fromParsedPromptFile(promptFile, extra)); + } + } + return result; + } + public async resolvePromptSlashCommand(name: string, sessionType: string, token: CancellationToken): Promise { const harness = this.findHarnessById(sessionType); if (!harness || !harness.itemProvider) { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index 70735295b98fd..17fd0c835e7eb 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -196,7 +196,6 @@ export interface IPluginPromptPath extends IPromptPathBase { export type IAgentSource = { readonly storage: PromptsStorage.extension; readonly extensionId: ExtensionIdentifier; - readonly type: PromptFileSource.ExtensionContribution | PromptFileSource.ExtensionAPI; } | { readonly storage: PromptsStorage.local | PromptsStorage.user; } | { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index ef662ec845fb7..74de7de710220 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -791,73 +791,24 @@ export class PromptsService extends Disposable implements IPromptsService { try { const ast = await this.parseNew(uri, token); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let metadata: any | undefined; - if (ast.header) { - const advanced = ast.header.getAttribute(PromptHeaderAttributes.advancedOptions); - if (advanced && advanced.value.type === 'map') { - metadata = {}; - for (const [key, value] of Object.entries(advanced.value)) { - if (value.type === 'scalar') { - metadata[key] = value; - } - } - } - } - const toolReferences: IVariableReference[] = []; - if (ast.body) { - const bodyOffset = ast.body.offset; - const bodyVarRefs = ast.body.variableReferences; - for (let i = bodyVarRefs.length - 1; i >= 0; i--) { // in reverse order - const { name, offset, fullLength } = bodyVarRefs[i]; - const range = new OffsetRange(offset - bodyOffset, offset - bodyOffset + fullLength); - toolReferences.push({ name, range }); - } - } - - const agentInstructions = { - content: ast.body?.getContent() ?? '', - toolReferences, - metadata, - } satisfies IAgentInstructions; - - const name = ast.header?.name ?? promptPath.name ?? getCleanPromptName(uri); - const description = ast.header?.description ?? promptPath.description; - const target = getTarget(PromptsType.agent, ast.header ?? uri); - - const source: IAgentSource = IAgentSource.fromPromptPath(promptPath); - const when = isExtensionPromptPath(promptPath) && promptPath.when - ? ContextKeyExpr.deserialize(promptPath.when) ?? undefined - : undefined; - if (!ast.header) { - const agent: ICustomAgent = { uri, name, agentInstructions, source, target, visibility: { userInvocable: true, agentInvocable: true }, sessionTypes: promptPath.sessionTypes, ...(when !== undefined ? { when } : undefined) }; - return { status: 'loaded', promptPath: this.withPromptPathMetadata(promptPath, name, description), agent }; - } - const visibility = { - userInvocable: ast.header.userInvocable !== false, - agentInvocable: ast.header.infer !== undefined ? ast.header.infer === true : ast.header.disableModelInvocation !== true, - } satisfies ICustomAgentVisibility; - - let model = ast.header.model; - if (target === Target.Claude && model) { - model = mapClaudeModels(model); - } - let { tools, handOffs, argumentHint, agents } = ast.header; - if (target === Target.Claude && tools) { - tools = mapClaudeTools(tools); - } - // Parse hooks from the frontmatter if present let hooks: ChatRequestHooks | undefined; - const hooksRaw = ast.header.hooksRaw; + const hooksRaw = ast.header?.hooksRaw; if (useChatHooks && isWorkspaceTrusted && hooksRaw) { const hookWorkspaceFolder = this.workspaceService.getWorkspaceFolder(uri) ?? defaultFolder; const workspaceRootUri = hookWorkspaceFolder?.uri; + const target = getTarget(PromptsType.agent, ast.header ?? promptPath.uri); hooks = parseSubagentHooksFromYaml(hooksRaw, workspaceRootUri, userHome, target); } - - const agent: ICustomAgent = { uri, name, description, model, tools, handOffs, argumentHint, target, visibility, agents, hooks, agentInstructions, source, sessionTypes: promptPath.sessionTypes, ...(when !== undefined ? { when } : undefined) }; - return { status: 'loaded', promptPath: this.withPromptPathMetadata(promptPath, name, description), agent }; + const extra = { + sessionTypes: promptPath.sessionTypes, + hooks, + name: promptPath.name, + description: promptPath.description, + source: IAgentSource.fromPromptPath(promptPath) + }; + const agent = CustomAgent.fromParsedPromptFile(ast, extra); + return { status: 'loaded', promptPath: this.withPromptPathMetadata(promptPath, agent.name, agent.description), agent }; } catch (e) { const error = e instanceof Error ? e : new Error(String(e)); if (error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { @@ -1788,13 +1739,70 @@ class ModelChangeTracker extends Disposable { } } +export namespace CustomAgent { + export function fromParsedPromptFile(ast: ParsedPromptFile, extra: { name?: string; description?: string; when?: string; source: IAgentSource; hooks?: ChatRequestHooks; sessionTypes: readonly string[] | undefined }): ICustomAgent { + const uri = ast.uri; + const { hooks, sessionTypes } = extra; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let metadata: any | undefined; + if (ast.header) { + const advanced = ast.header.getAttribute(PromptHeaderAttributes.advancedOptions); + if (advanced && advanced.value.type === 'map') { + metadata = {}; + for (const [key, value] of Object.entries(advanced.value)) { + if (value.type === 'scalar') { + metadata[key] = value; + } + } + } + } + const toolReferences: IVariableReference[] = []; + if (ast.body) { + const bodyOffset = ast.body.offset; + const bodyVarRefs = ast.body.variableReferences; + for (let i = bodyVarRefs.length - 1; i >= 0; i--) { // in reverse order + const { name, offset, fullLength } = bodyVarRefs[i]; + const range = new OffsetRange(offset - bodyOffset, offset - bodyOffset + fullLength); + toolReferences.push({ name, range }); + } + } + + const agentInstructions = { content: ast.body?.getContent() ?? '', toolReferences, metadata } satisfies IAgentInstructions; + + const name = ast.header?.name ?? extra.name ?? getCleanPromptName(uri); + const description = ast.header?.description ?? extra.description; + const target = getTarget(PromptsType.agent, ast.header ?? uri); + + const when = extra.when ? ContextKeyExpr.deserialize(extra.when) ?? undefined : undefined; + const source = extra.source; + if (!ast.header) { + return { uri, name, agentInstructions, source, target, visibility: { userInvocable: true, agentInvocable: true }, sessionTypes, hooks, when }; + } + const visibility = { + userInvocable: ast.header.userInvocable !== false, + agentInvocable: ast.header.infer !== undefined ? ast.header.infer === true : ast.header.disableModelInvocation !== true, + } satisfies ICustomAgentVisibility; + + let model = ast.header.model; + if (target === Target.Claude && model) { + model = mapClaudeModels(model); + } + let { tools, handOffs, argumentHint, agents } = ast.header; + if (target === Target.Claude && tools) { + tools = mapClaudeTools(tools); + } + return { uri, name, description, model, tools, handOffs, argumentHint, target, visibility, agents, agentInstructions, source, sessionTypes, hooks, when }; + + } +} + namespace IAgentSource { export function fromPromptPath(promptPath: IPromptPath): IAgentSource { if (promptPath.storage === PromptsStorage.extension) { return { storage: PromptsStorage.extension, - extensionId: promptPath.extension.identifier, - type: promptPath.source + extensionId: promptPath.extension.identifier }; } else if (promptPath.storage === PromptsStorage.plugin) { return { diff --git a/src/vs/workbench/contrib/chat/test/common/customizationHarnessService.test.ts b/src/vs/workbench/contrib/chat/test/common/customizationHarnessService.test.ts index 4eb4cdffc88f3..d0a384e9aec7e 100644 --- a/src/vs/workbench/contrib/chat/test/common/customizationHarnessService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/customizationHarnessService.test.ts @@ -9,8 +9,8 @@ import { URI } from '../../../../../base/common/uri.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { CustomizationHarnessServiceBase, createVSCodeHarnessDescriptor, ICustomizationItemProvider, IHarnessDescriptor, matchesWorkspaceSubpath } from '../../common/customizationHarnessService.js'; -import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; -import { IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; +import { PromptsType, Target } from '../../common/promptSyntax/promptTypes.js'; +import { ICustomAgent, IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { SessionType } from '../../common/chatSessionsService.js'; import { MockPromptsService } from './promptSyntax/service/mockPromptsService.js'; @@ -55,6 +55,32 @@ suite('CustomizationHarnessService', () => { assert.strictEqual(firedSessionType, harnessId); }); + test('forwards item provider changes via onDidChangeCustomAgents with sessionType', () => { + const service = createService(); + const emitter = new Emitter(); + store.add(emitter); + const harnessId = 'test-harness'; + const externalDescriptor: IHarnessDescriptor = { + id: harnessId, + label: 'Test Harness', + icon: ThemeIcon.fromId('extensions'), + getStorageSourceFilter: () => ({ sources: [PromptsStorage.local] }), + itemProvider: { + onDidChange: emitter.event, + provideChatSessionCustomizations: async () => [], + }, + }; + + store.add(service.registerExternalHarness(externalDescriptor)); + + let firedSessionType: string | undefined; + const listener = store.add(service.onDidChangeCustomAgents(e => firedSessionType = e.sessionType)); + store.add(listener); + + emitter.fire(); + assert.strictEqual(firedSessionType, harnessId); + }); + test('adds harness to available list', () => { const service = createService(); assert.strictEqual(service.availableHarnesses.get().length, 1); @@ -175,7 +201,7 @@ suite('CustomizationHarnessService', () => { const emitter = new Emitter(); store.add(emitter); const testItems = [ - { uri: URI.parse('file:///workspace/.claude/SKILL.md'), type: 'skill', name: 'Test Skill', description: 'A test skill' }, + { uri: URI.parse('file:///workspace/.claude/SKILL.md'), type: 'skill', name: 'Test Skill', description: 'A test skill', extensionId: undefined, pluginUri: undefined }, ]; const itemProvider: ICustomizationItemProvider = { @@ -346,10 +372,10 @@ suite('CustomizationHarnessService', () => { itemProvider: { onDidChange: emitter.event, provideChatSessionCustomizations: async () => [ - { uri: URI.parse('file:///workspace/.test/prompts/fix.prompt.md'), type: PromptsType.prompt, name: 'fix', description: 'Fix something' }, - { uri: URI.parse('file:///workspace/.test/skills/lint/SKILL.md'), type: PromptsType.skill, name: 'lint', description: 'Lint skill' }, - { uri: URI.parse('file:///workspace/.test/instructions/rule.instructions.md'), type: PromptsType.instructions, name: 'rule', description: 'Ignore me' }, - { uri: URI.parse('file:///workspace/.test/skills/disabled/SKILL.md'), type: PromptsType.skill, name: 'disabled', enabled: false }, + { uri: URI.parse('file:///workspace/.test/prompts/fix.prompt.md'), type: PromptsType.prompt, name: 'fix', description: 'Fix something', extensionId: undefined, pluginUri: undefined }, + { uri: URI.parse('file:///workspace/.test/skills/lint/SKILL.md'), type: PromptsType.skill, name: 'lint', description: 'Lint skill', extensionId: undefined, pluginUri: undefined }, + { uri: URI.parse('file:///workspace/.test/instructions/rule.instructions.md'), type: PromptsType.instructions, name: 'rule', description: 'Ignore me', extensionId: undefined, pluginUri: undefined }, + { uri: URI.parse('file:///workspace/.test/skills/disabled/SKILL.md'), type: PromptsType.skill, name: 'disabled', enabled: false, extensionId: undefined, pluginUri: undefined }, ], }, }); @@ -391,6 +417,62 @@ suite('CustomizationHarnessService', () => { }); }); + suite('getCustomAgents', () => { + const createAgent = (name: string, path: string, sessionTypes?: readonly string[]): ICustomAgent => ({ + uri: URI.parse(path), + name, + target: Target.GitHubCopilot, + visibility: { userInvocable: true, agentInvocable: true }, + agentInstructions: { content: '', toolReferences: [] }, + source: { storage: PromptsStorage.local }, + sessionTypes, + }); + + test('falls back to promptsService and filters by session type', async () => { + const testSessionType = 'test-session-type'; + const promptsService = new MockPromptsService(); + promptsService.setCustomModes([ + createAgent('matching', 'file:///workspace/.github/agents/matching.agent.md', [testSessionType]), + createAgent('global', 'file:///workspace/.github/agents/global.agent.md'), + createAgent('other', 'file:///workspace/.github/agents/other.agent.md', ['other-session']), + ]); + const service = new CustomizationHarnessServiceBase([createVSCodeHarnessDescriptor([PromptsStorage.extension])], SessionType.Local, promptsService); + store.add(service); + + const agents = await service.getCustomAgents(testSessionType, CancellationToken.None); + assert.deepStrictEqual(agents.map(agent => agent.name), ['matching', 'global']); + }); + + test('uses provider item URIs to scope resolved custom agents', async () => { + const testSessionType = 'test-session-type'; + const promptsService = new MockPromptsService(); + promptsService.setCustomModes([ + createAgent('selected', 'file:///workspace/.test/agents/selected.agent.md', [testSessionType]), + createAgent('not-selected', 'file:///workspace/.test/agents/not-selected.agent.md', [testSessionType]), + ]); + + const emitter = new Emitter(); + store.add(emitter); + const service = new CustomizationHarnessServiceBase([{ + id: testSessionType, + label: 'Test Extension', + icon: ThemeIcon.fromId('extensions'), + getStorageSourceFilter: () => ({ sources: [PromptsStorage.local] }), + itemProvider: { + onDidChange: emitter.event, + provideChatSessionCustomizations: async () => [ + { uri: URI.parse('file:///workspace/.test/agents/selected.agent.md'), type: PromptsType.agent, name: 'selected', extensionId: undefined, pluginUri: undefined }, + { uri: URI.parse('file:///workspace/.test/agents/disabled.agent.md'), type: PromptsType.agent, name: 'disabled', enabled: false, extensionId: undefined, pluginUri: undefined }, + ], + }, + }], testSessionType, promptsService); + store.add(service); + + const agents = await service.getCustomAgents(testSessionType, CancellationToken.None); + assert.deepStrictEqual(agents.map(agent => agent.name), ['selected']); + }); + }); + suite('matchesWorkspaceSubpath', () => { test('matches segment boundary', () => { assert.ok(matchesWorkspaceSubpath('/workspace/.claude/skills/SKILL.md', ['.claude'])); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts index c4e34dcec8916..47828f1ad080e 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts @@ -51,7 +51,7 @@ export class MockPromptsService implements IPromptsService { // eslint-disable-next-line @typescript-eslint/no-explicit-any parse(_uri: URI, _type: any, _token: CancellationToken): Promise { throw new Error('Not implemented'); } // eslint-disable-next-line @typescript-eslint/no-explicit-any - parseNew(_uri: URI, _token: CancellationToken): Promise { throw new Error('Not implemented'); } + parseNew(uri: URI, _token: CancellationToken): Promise { return Promise.resolve({ uri }); } getParsedPromptFile(textModel: ITextModel): ParsedPromptFile { throw new Error('Not implemented'); } registerContributedFile(type: PromptsType, uri: URI, extension: IExtensionDescription, name: string | undefined, description: string | undefined, when?: string, sessionTypes?: readonly string[]): IDisposable { throw new Error('Not implemented'); } getPromptLocationLabel(promptPath: IPromptPath): string { throw new Error('Not implemented'); } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index 743114b8eaf5e..fc58f19982daf 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -811,7 +811,7 @@ suite('PromptsService', () => { } ]); - const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + const result = (await service.getCustomAgents(CancellationToken.None)).map(({ when, ...agent }) => ({ ...agent, uri: URI.from(agent.uri) })); const expected: ICustomAgent[] = [ { name: 'agent1', @@ -869,7 +869,7 @@ suite('PromptsService', () => { } ]); - const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + const result = (await service.getCustomAgents(CancellationToken.None)).map(({ when, ...agent }) => ({ ...agent, uri: URI.from(agent.uri) })); const expected: ICustomAgent[] = [ { name: 'agent1', @@ -901,6 +901,7 @@ suite('PromptsService', () => { ], metadata: undefined }, + hooks: undefined, sessionTypes: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent2.agent.md'), source: { storage: PromptsStorage.local }, @@ -947,7 +948,7 @@ suite('PromptsService', () => { } ]); - const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + const result = (await service.getCustomAgents(CancellationToken.None)).map(({ when, ...agent }) => ({ ...agent, uri: URI.from(agent.uri) })); const expected: ICustomAgent[] = [ { name: 'agent1', @@ -1039,7 +1040,7 @@ suite('PromptsService', () => { } ]); - const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + const result = (await service.getCustomAgents(CancellationToken.None)).map(({ when, ...agent }) => ({ ...agent, uri: URI.from(agent.uri) })); const expected: ICustomAgent[] = [ { name: 'github-agent', @@ -1157,7 +1158,7 @@ suite('PromptsService', () => { }, ]); - const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + const result = (await service.getCustomAgents(CancellationToken.None)).map(({ when, ...agent }) => ({ ...agent, uri: URI.from(agent.uri) })); const expected: ICustomAgent[] = [ { name: 'copilot-agent', @@ -1259,7 +1260,7 @@ suite('PromptsService', () => { } ]); - const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + const result = (await service.getCustomAgents(CancellationToken.None)).map(({ when, ...agent }) => ({ ...agent, uri: URI.from(agent.uri) })); const expected: ICustomAgent[] = [ { name: 'demonstrate', @@ -1331,7 +1332,7 @@ suite('PromptsService', () => { } ]); - const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + const result = (await service.getCustomAgents(CancellationToken.None)).map(({ when, ...agent }) => ({ ...agent, uri: URI.from(agent.uri) })); const expected: ICustomAgent[] = [ { name: 'restricted-agent', diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 8073fa472f1f6..9822c1333c631 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -3,27 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './inlineChatDefaultModel.js'; - import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; import { IMenuItem, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { InlineChatController } from './inlineChatController.js'; import * as InlineChatActions from './inlineChatActions.js'; import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_V1_ENABLED, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { Registry } from '../../../../platform/registry/common/platform.js'; -import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { InlineChatNotebookContribution } from './inlineChatNotebook.js'; -import { IWorkbenchContributionsRegistry, registerWorkbenchContribution2, Extensions as WorkbenchExtensions, WorkbenchPhase } from '../../../common/contributions.js'; +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { IInlineChatSessionService } from './inlineChatSessionService.js'; import { InlineChatEnabler, InlineChatEscapeToolContribution, InlineChatSessionServiceImpl } from './inlineChatSessionServiceImpl.js'; - import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { CancelAction, ChatSubmitAction } from '../../chat/browser/actions/chatExecuteActions.js'; import { localize } from '../../../../nls.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { InlineChatAccessibilityHelp } from './inlineChatAccessibilityHelp.js'; +import { InlineChatDefaultModel } from './inlineChatDefaultModel.js'; registerEditorContribution(InlineChatController.ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors @@ -97,10 +93,10 @@ registerAction2(InlineChatActions.FocusInlineChat); registerAction2(InlineChatActions.FixDiagnosticsAction); registerAction2(InlineChatActions.DismissEditorAffordanceAction); - -const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); -workbenchContributionsRegistry.registerWorkbenchContribution(InlineChatNotebookContribution, LifecyclePhase.Restored); - +// --- contribs --- +registerWorkbenchContribution2('inlineChat.notebooks', InlineChatNotebookContribution, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(InlineChatDefaultModel.ID, InlineChatDefaultModel, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(InlineChatEnabler.Id, InlineChatEnabler, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(InlineChatEscapeToolContribution.Id, InlineChatEscapeToolContribution, WorkbenchPhase.AfterRestored); + AccessibleViewRegistry.register(new InlineChatAccessibilityHelp()); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 84974401e3f67..cb56e1c70e55b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -5,13 +5,10 @@ import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; import { alert } from '../../../../base/browser/ui/aria/aria.js'; -import { raceCancellation } from '../../../../base/common/async.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; import { onUnexpectedError } from '../../../../base/common/errors.js'; import { Event } from '../../../../base/common/event.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../base/common/network.js'; import { autorun, derived, IObservable, observableFromEvent, observableSignalFromEvent, observableValue, waitForState } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; import { assertType } from '../../../../base/common/types.js'; @@ -23,24 +20,18 @@ import { IPosition, Position } from '../../../../editor/common/core/position.js' import { IRange, Range } from '../../../../editor/common/core/range.js'; import { ISelection, Selection } from '../../../../editor/common/core/selection.js'; import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; -import { TextEdit } from '../../../../editor/common/languages.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { IMarkerDecorationsService } from '../../../../editor/common/services/markerDecorations.js'; -import { EditSuggestionId } from '../../../../editor/common/textModelEditSource.js'; import { localize } from '../../../../nls.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; -import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js'; import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; -import { IChatAttachmentResolveService } from '../../chat/browser/attachments/chatAttachmentResolveService.js'; import { IChatWidgetLocationOptions } from '../../chat/browser/widget/chatWidget.js'; import { IChatEditingService, ModifiedFileEntryState } from '../../chat/common/editing/chatEditingService.js'; -import { ChatModel } from '../../chat/common/model/chatModel.js'; import { ChatMode } from '../../chat/common/chatModes.js'; import { IChatService, IChatToolInvocation, ToolConfirmKind } from '../../chat/common/chatService/chatService.js'; import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from '../../chat/common/attachments/chatVariableEntries.js'; @@ -49,12 +40,10 @@ import { ChatAgentLocation } from '../../chat/common/constants.js'; import { ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsService, isILanguageModelChatSelector } from '../../chat/common/languageModels.js'; import { isNotebookContainingCellEditor as isNotebookWithCellEditor } from '../../notebook/browser/notebookEditor.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; -import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommon.js'; -import { INotebookService } from '../../notebook/common/notebookService.js'; +import { CellUri } from '../../notebook/common/notebookCommon.js'; import { CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, CTX_INLINE_CHAT_TERMINATED, CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; import { InlineChatAffordance } from './inlineChatAffordance.js'; - -import { continueInPanelChat, IInlineChatSession2, IInlineChatSessionService, rephraseInlineChat } from './inlineChatSessionService.js'; +import { continueInPanelChat, IInlineChatSession, IInlineChatSessionService, rephraseInlineChat } from './inlineChatSessionService.js'; import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; @@ -119,16 +108,13 @@ export class InlineChatController implements IEditorContribution { readonly #zone: Lazy; readonly inputOverlayWidget: InlineChatAffordance; - readonly #currentSession: IObservable; + readonly #currentSession: IObservable; readonly #editor: ICodeEditor; readonly #instaService: IInstantiationService; readonly #notebookEditorService: INotebookEditorService; readonly #inlineChatSessionService: IInlineChatSessionService; readonly #configurationService: IConfigurationService; - readonly #webContentExtractorService: ISharedWebContentExtractorService; - readonly #fileService: IFileService; - readonly #chatAttachmentResolveService: IChatAttachmentResolveService; readonly #editorService: IEditorService; readonly #markerDecorationsService: IMarkerDecorationsService; readonly #languageModelService: ILanguageModelsService; @@ -152,9 +138,6 @@ export class InlineChatController implements IEditorContribution { @ICodeEditorService codeEditorService: ICodeEditorService, @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService configurationService: IConfigurationService, - @ISharedWebContentExtractorService webContentExtractorService: ISharedWebContentExtractorService, - @IFileService fileService: IFileService, - @IChatAttachmentResolveService chatAttachmentResolveService: IChatAttachmentResolveService, @IEditorService editorService: IEditorService, @IMarkerDecorationsService markerDecorationsService: IMarkerDecorationsService, @ILanguageModelsService languageModelService: ILanguageModelsService, @@ -167,9 +150,6 @@ export class InlineChatController implements IEditorContribution { this.#notebookEditorService = notebookEditorService; this.#inlineChatSessionService = inlineChatSessionService; this.#configurationService = configurationService; - this.#webContentExtractorService = webContentExtractorService; - this.#fileService = fileService; - this.#chatAttachmentResolveService = chatAttachmentResolveService; this.#editorService = editorService; this.#markerDecorationsService = markerDecorationsService; this.#languageModelService = languageModelService; @@ -182,7 +162,7 @@ export class InlineChatController implements IEditorContribution { const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); const ctxFileBelongsToChat = CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.bindTo(contextKeyService); const ctxTerminated = CTX_INLINE_CHAT_TERMINATED.bindTo(contextKeyService); - const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this.#configurationService); + const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.NotebookAgent, false, this.#configurationService); // Track whether the current editor's file is being edited by any chat editing session this.#store.add(autorun(r => { @@ -290,7 +270,7 @@ export class InlineChatController implements IEditorContribution { }); - let lastSession: IInlineChatSession2 | undefined = undefined; + let lastSession: IInlineChatSession | undefined = undefined; this.#store.add(autorun(r => { const session = this.#currentSession.read(r); @@ -322,7 +302,7 @@ export class InlineChatController implements IEditorContribution { } })); - const visibleSessionObs = observableValue(this, undefined); + const visibleSessionObs = observableValue(this, undefined); this.#store.add(autorun(r => { @@ -541,7 +521,7 @@ export class InlineChatController implements IEditorContribution { /** * Zone mode: use the full zone widget and chat widget for request submission. */ - async #runZone(session: IInlineChatSession2, arg?: InlineChatRunOptions): Promise { + async #runZone(session: IInlineChatSession, arg?: InlineChatRunOptions): Promise { assertType(this.#editor.hasModel()); const uri = this.#editor.getModel().uri; @@ -671,7 +651,7 @@ export class InlineChatController implements IEditorContribution { this.#zone.rawValue?.widget.focus(); } - async #selectVendorDefaultModel(session: IInlineChatSession2): Promise { + async #selectVendorDefaultModel(session: IInlineChatSession): Promise { const model = this.#zone.value.widget.chatWidget.input.selectedLanguageModel.get(); if (model && !model.metadata.isDefaultForLocation[session.chatModel.initialLocation]) { const ids = await this.#languageModelService.selectLanguageModels({ vendor: model.metadata.vendor }); @@ -689,7 +669,7 @@ export class InlineChatController implements IEditorContribution { * Applies model defaults based on settings and tracks user model changes. * Prioritization: user session choice > inlineChat.defaultModel setting > vendor default */ - async #applyModelDefaults(session: IInlineChatSession2, sessionStore: DisposableStore): Promise { + async #applyModelDefaults(session: IInlineChatSession, sessionStore: DisposableStore): Promise { const userSelectedModel = InlineChatController.#userSelectedModel; const defaultModelSetting = this.#configurationService.getValue(InlineChatConfigKeys.DefaultModel); @@ -736,147 +716,4 @@ export class InlineChatController implements IEditorContribution { } })); } - - async createImageAttachment(attachment: URI): Promise { - const value = this.#currentSession.get(); - if (!value) { - return undefined; - } - if (attachment.scheme === Schemas.file) { - if (await this.#fileService.canHandleResource(attachment)) { - return await this.#chatAttachmentResolveService.resolveImageEditorAttachContext(attachment); - } - } else if (attachment.scheme === Schemas.http || attachment.scheme === Schemas.https) { - const extractedImages = await this.#webContentExtractorService.readImage(attachment, CancellationToken.None); - if (extractedImages) { - return await this.#chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages); - } - } - return undefined; - } -} - -export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEditor, stream: AsyncIterable, token: CancellationToken, applyCodeBlockSuggestionId: EditSuggestionId | undefined): Promise { - if (!editor.hasModel()) { - return false; - } - - const chatService = accessor.get(IChatService); - const uri = editor.getModel().uri; - const chatModelRef = chatService.startNewLocalSession(ChatAgentLocation.EditorInline); - const chatModel = chatModelRef.object as ChatModel; - - chatModel.startEditingSession(true); - - const store = new DisposableStore(); - store.add(chatModelRef); - - // STREAM - const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0, { - kind: undefined, - modeId: 'applyCodeBlock', - modeInstructions: undefined, - isBuiltin: true, - applyCodeBlockSuggestionId, - }); - assertType(chatRequest.response); - chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false }); - for await (const chunk of stream) { - - if (token.isCancellationRequested) { - chatRequest.response.cancel(); - break; - } - - chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: chunk, done: false }); - } - chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); - - if (!token.isCancellationRequested) { - chatRequest.response.complete(); - } - - const isSettled = derived(r => { - const entry = chatModel.editingSession?.readEntry(uri, r); - if (!entry) { - return false; - } - const state = entry.state.read(r); - return state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected; - }); - const whenDecided = waitForState(isSettled, Boolean); - await raceCancellation(whenDecided, token); - store.dispose(); - return true; -} - -export async function reviewNotebookEdits(accessor: ServicesAccessor, uri: URI, stream: AsyncIterable<[URI, TextEdit[]] | ICellEditOperation[]>, token: CancellationToken): Promise { - - const chatService = accessor.get(IChatService); - const notebookService = accessor.get(INotebookService); - const isNotebook = notebookService.hasSupportedNotebooks(uri); - const chatModelRef = chatService.startNewLocalSession(ChatAgentLocation.EditorInline); - const chatModel = chatModelRef.object as ChatModel; - - chatModel.startEditingSession(true); - - const store = new DisposableStore(); - store.add(chatModelRef); - - // STREAM - const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0); - assertType(chatRequest.response); - if (isNotebook) { - chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: [], done: false }); - } else { - chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false }); - } - for await (const chunk of stream) { - - if (token.isCancellationRequested) { - chatRequest.response.cancel(); - break; - } - if (chunk.every(isCellEditOperation)) { - chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: chunk, done: false }); - } else { - chatRequest.response.updateContent({ kind: 'textEdit', uri: chunk[0], edits: chunk[1], done: false }); - } - } - if (isNotebook) { - chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: [], done: true }); - } else { - chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); - } - - if (!token.isCancellationRequested) { - chatRequest.response.complete(); - } - - const isSettled = derived(r => { - const entry = chatModel.editingSession?.readEntry(uri, r); - if (!entry) { - return false; - } - const state = entry.state.read(r); - return state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected; - }); - - const whenDecided = waitForState(isSettled, Boolean); - - await raceCancellation(whenDecided, token); - - store.dispose(); - - return true; -} - -function isCellEditOperation(edit: URI | TextEdit[] | ICellEditOperation): edit is ICellEditOperation { - if (URI.isUri(edit)) { - return false; - } - if (Array.isArray(edit)) { - return false; - } - return true; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatDefaultModel.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatDefaultModel.ts index 5fd53270237b5..29ec10f5272b9 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatDefaultModel.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatDefaultModel.ts @@ -7,7 +7,6 @@ import { localize } from '../../../../nls.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../../platform/configuration/common/configurationRegistry.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; -import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { ILanguageModelsService } from '../../chat/common/languageModels.js'; import { InlineChatConfigKeys } from '../common/inlineChat.js'; import { createDefaultModelArrays, DefaultModelContribution } from '../../chat/browser/defaultModelContribution.js'; @@ -34,7 +33,6 @@ export class InlineChatDefaultModel extends DefaultModelContribution { } } -registerWorkbenchContribution2(InlineChatDefaultModel.ID, InlineChatDefaultModel, WorkbenchPhase.BlockRestore); Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ ...{ id: 'inlineChat', title: localize('inlineChatConfigurationTitle', 'Inline Chat'), order: 30, type: 'object' }, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts index ca722843a32e6..bf1d2ea10aa18 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts @@ -10,7 +10,6 @@ import { InlineChatController } from './inlineChatController.js'; import { IInlineChatSessionService } from './inlineChatSessionService.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; import { CellUri } from '../../notebook/common/notebookCommon.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; export class InlineChatNotebookContribution { @@ -18,7 +17,6 @@ export class InlineChatNotebookContribution { constructor( @IInlineChatSessionService sessionService: IInlineChatSessionService, - @IEditorService editorService: IEditorService, @INotebookEditorService notebookEditorService: INotebookEditorService, ) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts index 8a348c9fba5a2..1fb914877cf01 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts @@ -21,7 +21,7 @@ export const IInlineChatSessionService = createDecorator; readonly onDidChangeSessions: Event; - dispose(): void; - - createSession(editor: ICodeEditor): IInlineChatSession2; - getSessionByTextModel(uri: URI): IInlineChatSession2 | undefined; - getSessionBySessionUri(uri: URI): IInlineChatSession2 | undefined; -} - -export async function moveToPanelChat(accessor: ServicesAccessor, model: IChatModel | undefined, resend: boolean) { - - const chatService = accessor.get(IChatService); - const widgetService = accessor.get(IChatWidgetService); - - const widget = await widgetService.revealWidget(); - - if (widget && widget.viewModel && model) { - let lastRequest: IChatRequestModel | undefined; - for (const request of model.getRequests().slice()) { - await chatService.adoptRequest(widget.viewModel.model.sessionResource, request); - lastRequest = request; - } - - if (lastRequest && resend) { - chatService.resendRequest(lastRequest, { location: widget.location }); - } - - widget.focusResponseItem(); - } + createSession(editor: ICodeEditor): IInlineChatSession; + getSessionByTextModel(uri: URI): IInlineChatSession | undefined; + getSessionBySessionUri(uri: URI): IInlineChatSession | undefined; } -export async function askInPanelChat(accessor: ServicesAccessor, request: IChatRequestModel, state: IChatModelInputState | undefined, fileContext?: { uri: URI; selection: Selection }) { +async function askInPanelChat(accessor: ServicesAccessor, request: IChatRequestModel, state: IChatModelInputState | undefined, fileContext?: { uri: URI; selection: Selection }) { const widgetService = accessor.get(IChatWidgetService); const chatService = accessor.get(IChatService); @@ -94,7 +70,7 @@ export async function askInPanelChat(accessor: ServicesAccessor, request: IChatR widget?.acceptInput(request.message.text); } -export async function continueInPanelChat(accessor: ServicesAccessor, session: IInlineChatSession2): Promise { +export async function continueInPanelChat(accessor: ServicesAccessor, session: IInlineChatSession): Promise { const request = session.chatModel.getRequests().at(-1); if (!request) { return; @@ -104,7 +80,7 @@ export async function continueInPanelChat(accessor: ServicesAccessor, session: I session.dispose(); } -export function rephraseInlineChat(accessor: ServicesAccessor, session: IInlineChatSession2): string | undefined { +export function rephraseInlineChat(accessor: ServicesAccessor, session: IInlineChatSession): string | undefined { const request = session.chatModel.getRequests().at(-1); if (!request) { return undefined; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index e18149801cba7..3c9d7cb8cc430 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -21,7 +21,7 @@ import { IChatService } from '../../chat/common/chatService/chatService.js'; import { ChatAgentLocation } from '../../chat/common/constants.js'; import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../chat/common/tools/languageModelToolsService.js'; import { CTX_INLINE_CHAT_HAS_AGENT2, CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT, CTX_INLINE_CHAT_POSSIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; -import { IInlineChatSession2, IInlineChatSessionService, InlineChatSessionTerminationState } from './inlineChatSessionService.js'; +import { IInlineChatSession, IInlineChatSessionService, InlineChatSessionTerminationState } from './inlineChatSessionService.js'; export class InlineChatError extends Error { static readonly code = 'InlineChatError'; @@ -36,7 +36,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { declare _serviceBrand: undefined; readonly #store = new DisposableStore(); - readonly #sessions = new ResourceMap(); + readonly #sessions = new ResourceMap(); readonly #onWillStartSession = this.#store.add(new Emitter()); readonly onWillStartSession: Event = this.#onWillStartSession.event; @@ -68,7 +68,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { } - createSession(editor: IActiveCodeEditor): IInlineChatSession2 { + createSession(editor: IActiveCodeEditor): IInlineChatSession { const uri = editor.getModel().uri; if (this.#sessions.has(uri)) { @@ -128,7 +128,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { } })); - const result: IInlineChatSession2 = { + const result: IInlineChatSession = { uri, initialPosition: editor.getSelection().getStartPosition().delta(-1), /* one line above selection start */ initialSelection: editor.getSelection(), @@ -146,7 +146,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { return result; } - getSessionByTextModel(uri: URI): IInlineChatSession2 | undefined { + getSessionByTextModel(uri: URI): IInlineChatSession | undefined { let result = this.#sessions.get(uri); if (!result) { // no direct session, try to find an editing session which has a file entry for the uri @@ -161,7 +161,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { return result; } - getSessionBySessionUri(sessionResource: URI): IInlineChatSession2 | undefined { + getSessionBySessionUri(sessionResource: URI): IInlineChatSession | undefined { for (const session of this.#sessions.values()) { if (isEqual(session.chatModel.sessionResource, sessionResource)) { return session; @@ -193,7 +193,7 @@ export class InlineChatEnabler { const agentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline)); const notebookAgentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.Notebook)); - const notebookAgentConfigObs = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, configService); + const notebookAgentConfigObs = observableConfigValue(InlineChatConfigKeys.NotebookAgent, false, configService); this.#store.add(autorun(r => { const agent = agentObs.read(r); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 5fd88f1448453..1ee9a1f61f2a1 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -14,10 +14,7 @@ import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle import { autorun, IObservable, observableValue } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { Selection } from '../../../../editor/common/core/selection.js'; import { ICodeEditorViewState } from '../../../../editor/common/editorCommon.js'; -import { ITextModel } from '../../../../editor/common/model.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { localize } from '../../../../nls.js'; import { IAccessibleViewService } from '../../../../platform/accessibility/browser/accessibleView.js'; @@ -44,7 +41,6 @@ import { IChatWidgetViewOptions } from '../../chat/browser/chat.js'; import { ChatWidget, IChatWidgetLocationOptions } from '../../chat/browser/widget/chatWidget.js'; import { chatRequestBackground } from '../../chat/common/widget/chatColors.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; -import { IChatModel } from '../../chat/common/model/chatModel.js'; import { ChatMode } from '../../chat/common/chatModes.js'; import { ChatAgentVoteDirection, IChatService } from '../../chat/common/chatService/chatService.js'; import { isResponseVM } from '../../chat/common/model/chatViewModel.js'; @@ -74,7 +70,7 @@ export interface IInlineChatWidgetConstructionOptions { inZoneWidget?: boolean; } -export class InlineChatWidget { +export abstract class InlineChatWidget { protected readonly _elements = h( 'div.inline-chat@root', @@ -96,7 +92,7 @@ export class InlineChatWidget { readonly #ctxInputEditorFocused: IContextKey; readonly #ctxResponseFocused: IContextKey; - readonly #chatWidget: ChatWidget; + readonly chatWidget: ChatWidget; protected readonly _onDidChangeHeight = this._store.add(new Emitter()); readonly onDidChangeHeight: Event = Event.filter(this._onDidChangeHeight.event, _ => !this.#isLayouting); @@ -113,7 +109,6 @@ export class InlineChatWidget { readonly #accessibilityService: IAccessibilityService; readonly #configurationService: IConfigurationService; readonly #accessibleViewService: IAccessibleViewService; - readonly #modelService: IModelService; readonly #chatService: IChatService; readonly #chatEntitlementService: IChatEntitlementService; readonly #markdownRendererService: IMarkdownRendererService; @@ -128,7 +123,6 @@ export class InlineChatWidget { @IConfigurationService configurationService: IConfigurationService, @IAccessibleViewService accessibleViewService: IAccessibleViewService, @ITextModelService protected readonly _textModelResolverService: ITextModelService, - @IModelService modelService: IModelService, @IChatService chatService: IChatService, @IHoverService hoverService: IHoverService, @IChatEntitlementService chatEntitlementService: IChatEntitlementService, @@ -139,7 +133,6 @@ export class InlineChatWidget { this.#accessibilityService = accessibilityService; this.#configurationService = configurationService; this.#accessibleViewService = accessibleViewService; - this.#modelService = modelService; this.#chatService = chatService; this.#chatEntitlementService = chatEntitlementService; this.#markdownRendererService = markdownRendererService; @@ -153,7 +146,7 @@ export class InlineChatWidget { this._store ); - this.#chatWidget = scopedInstaService.createInstance( + this.chatWidget = scopedInstaService.createInstance( ChatWidget, location, { isInlineChat: true }, @@ -191,10 +184,10 @@ export class InlineChatWidget { } ); this._elements.root.classList.toggle('in-zone-widget', !!options.inZoneWidget); - this.#chatWidget.render(this._elements.chatWidget); + this.chatWidget.render(this._elements.chatWidget); this._elements.chatWidget.style.setProperty(asCssVariableName(chatRequestBackground), asCssVariable(inlineChatBackground)); - this.#chatWidget.setVisible(true); - this._store.add(this.#chatWidget); + this.chatWidget.setVisible(true); + this._store.add(this.chatWidget); const ctxResponse = ChatContextKeys.isResponse.bindTo(this.scopedContextKeyService); const ctxResponseVote = ChatContextKeys.responseVote.bindTo(this.scopedContextKeyService); @@ -203,10 +196,10 @@ export class InlineChatWidget { const ctxResponseErrorFiltered = ChatContextKeys.responseIsFiltered.bindTo(this.scopedContextKeyService); const viewModelStore = this._store.add(new DisposableStore()); - this._store.add(this.#chatWidget.onDidChangeViewModel(() => { + this._store.add(this.chatWidget.onDidChangeViewModel(() => { viewModelStore.clear(); - const viewModel = this.#chatWidget.viewModel; + const viewModel = this.chatWidget.viewModel; if (!viewModel) { return; } @@ -249,8 +242,8 @@ export class InlineChatWidget { this._store.add(tracker.onDidFocus(() => this.#ctxResponseFocused.set(true))); this.#ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(contextKeyService); - this._store.add(this.#chatWidget.inputEditor.onDidFocusEditorWidget(() => this.#ctxInputEditorFocused.set(true))); - this._store.add(this.#chatWidget.inputEditor.onDidBlurEditorWidget(() => this.#ctxInputEditorFocused.set(false))); + this._store.add(this.chatWidget.inputEditor.onDidFocusEditorWidget(() => this.#ctxInputEditorFocused.set(true))); + this._store.add(this.chatWidget.inputEditor.onDidBlurEditorWidget(() => this.#ctxInputEditorFocused.set(false))); const statusMenuId = options.statusMenuId instanceof MenuId ? options.statusMenuId : options.statusMenuId.menu; @@ -293,7 +286,7 @@ export class InlineChatWidget { })); this._store.add(this.#chatService.onDidPerformUserAction(e => { - if (isEqual(e.sessionResource, this.#chatWidget.viewModel?.model.sessionResource) && e.action.kind === 'vote') { + if (isEqual(e.sessionResource, this.chatWidget.viewModel?.model.sessionResource) && e.action.kind === 'vote') { this.updateStatus(localize('feedbackThanks', "Thank you for your feedback!"), { resetAfter: 1250 }); } })); @@ -311,7 +304,7 @@ export class InlineChatWidget { ? localize('inlineChat.accessibilityHelp', "Inline Chat Input, Use {0} for Inline Chat Accessibility Help.", kbLabel) : localize('inlineChat.accessibilityHelpNoKb', "Inline Chat Input, Run the Inline Chat Accessibility Help command for more information."); } - this.#chatWidget.inputEditor.updateOptions({ ariaLabel: label }); + this.chatWidget.inputEditor.updateOptions({ ariaLabel: label }); } } @@ -346,14 +339,6 @@ export class InlineChatWidget { return this._elements.root; } - get chatWidget(): ChatWidget { - return this.#chatWidget; - } - - saveState() { - this.#chatWidget.saveState(); - } - layout(widgetDim: Dimension) { const contentHeight = this.contentHeight; this.#isLayouting = true; @@ -377,7 +362,7 @@ export class InlineChatWidget { this._elements.root.style.height = `${dimension.height - extraHeight}px`; this._elements.root.style.width = `${dimension.width}px`; - this.#chatWidget.layout( + this.chatWidget.layout( dimension.height - statusHeight - extraHeight, dimension.width ); @@ -388,7 +373,7 @@ export class InlineChatWidget { */ get contentHeight(): number { const data = { - chatWidgetContentHeight: this.#chatWidget.contentHeight, + chatWidgetContentHeight: this.chatWidget.contentHeight, statusHeight: getTotalHeight(this._elements.status), extraHeight: this._getExtraHeight() }; @@ -401,7 +386,7 @@ export class InlineChatWidget { // at least "maxWidgetHeight" high and at most the content height. let maxWidgetOutputHeight = 100; - for (const item of this.#chatWidget.viewModel?.getItems() ?? []) { + for (const item of this.chatWidget.viewModel?.getItems() ?? []) { if (isResponseVM(item) && item.response.value.some(r => r.kind === 'textEditGroup' && !r.state?.applied)) { maxWidgetOutputHeight = 270; break; @@ -409,8 +394,8 @@ export class InlineChatWidget { } let value = this.contentHeight; - value -= this.#chatWidget.contentHeight; - value += Math.min(this.#chatWidget.input.height.get() + maxWidgetOutputHeight, this.#chatWidget.contentHeight); + value -= this.chatWidget.contentHeight; + value += Math.min(this.chatWidget.input.height.get() + maxWidgetOutputHeight, this.chatWidget.contentHeight); return value; } @@ -418,100 +403,6 @@ export class InlineChatWidget { return this.#options.inZoneWidget ? 1 : (2 /*border*/ + 4 /*shadow*/); } - get value(): string { - return this.#chatWidget.getInput(); - } - - set value(value: string) { - this.#chatWidget.setInput(value); - } - - selectAll() { - this.#chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1)); - } - - set placeholder(value: string) { - this.#chatWidget.setInputPlaceholder(value); - } - - toggleStatus(show: boolean) { - this._elements.toolbar1.classList.toggle('hidden', !show); - this._elements.toolbar2.classList.toggle('hidden', !show); - this._elements.status.classList.toggle('hidden', !show); - this._elements.infoLabel.classList.toggle('hidden', !show); - this._onDidChangeHeight.fire(); - } - - updateToolbar(show: boolean) { - this._elements.root.classList.toggle('toolbar', show); - this._elements.toolbar1.classList.toggle('hidden', !show); - this._elements.toolbar2.classList.toggle('hidden', !show); - this._elements.status.classList.toggle('actions', show); - this._elements.infoLabel.classList.toggle('hidden', show); - this._onDidChangeHeight.fire(); - } - - async getCodeBlockInfo(codeBlockIndex: number): Promise { - const { viewModel } = this.#chatWidget; - if (!viewModel) { - return undefined; - } - const items = viewModel.getItems().filter(i => isResponseVM(i)); - const item = items.at(-1); - if (!item) { - return; - } - - // Look for the code block in the rendered response - const codeBlocks = this.#chatWidget.getCodeBlockInfosForResponse(item); - const info = codeBlocks[codeBlockIndex]; - if (info?.uri) { - return this.#modelService.getModel(info.uri) ?? undefined; - } - - // Fallback: if the code block hasn't been rendered yet (e.g. due to - // timing between response completion and list rendering), parse the - // markdown directly and create a transient model. - const markdown = item.response.getMarkdown(); - let currentCodeBlockIndex = 0; - let foundText: string | undefined; - - for (const line of markdown.split('\n')) { - if (line.startsWith('```') && foundText === undefined) { - foundText = ''; - } else if (line.startsWith('```') && foundText !== undefined) { - if (currentCodeBlockIndex === codeBlockIndex) { - break; - } - currentCodeBlockIndex++; - foundText = undefined; - } else if (foundText !== undefined) { - foundText += (foundText ? '\n' : '') + line; - } - } - - if (foundText !== undefined && currentCodeBlockIndex === codeBlockIndex) { - return this.#modelService.createModel(foundText, null, undefined, true); - } - - return undefined; - } - - get responseContent(): string | undefined { - const requests = this.#chatWidget.viewModel?.model.getRequests(); - return requests?.at(-1)?.response?.response.toString(); - } - - - getChatModel(): IChatModel | undefined { - return this.#chatWidget.viewModel?.model; - } - - setChatModel(chatModel: IChatModel) { - chatModel.inputModel.setState({ inputText: '', selections: [] }); - this.#chatWidget.setModel(chatModel); - } - updateInfo(message: string): void { this._elements.infoLabel.classList.toggle('hidden', !message); const renderedMessage = renderLabelWithIcons(message); @@ -548,8 +439,8 @@ export class InlineChatWidget { } reset() { - this.#chatWidget.attachmentModel.clear(true); - this.#chatWidget.saveState(); + this.chatWidget.attachmentModel.clear(true); + this.chatWidget.saveState(); reset(this._elements.statusLabel); this._elements.statusLabel.classList.toggle('hidden', true); @@ -562,7 +453,7 @@ export class InlineChatWidget { } focus() { - this.#chatWidget.focusInput(); + this.chatWidget.focusInput(); } hasFocus() { @@ -586,7 +477,6 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { @IConfigurationService configurationService: IConfigurationService, @IAccessibleViewService accessibleViewService: IAccessibleViewService, @ITextModelService textModelResolverService: ITextModelService, - @IModelService modelService: IModelService, @IChatService chatService: IChatService, @IHoverService hoverService: IHoverService, @ILayoutService layoutService: ILayoutService, @@ -600,7 +490,7 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { ...options.chatWidgetViewOptions, editorOverflowWidgetsDomNode: overflowWidgetsNode } - }, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, textModelResolverService, modelService, chatService, hoverService, chatEntitlementService, markdownRendererService); + }, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, textModelResolverService, chatService, hoverService, chatEntitlementService, markdownRendererService); this._store.add(toDisposable(() => { overflowWidgetsNode.remove(); diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 9a05e4cec1097..689a0c6d01eb5 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -14,10 +14,9 @@ import { NOTEBOOK_IS_ACTIVE_EDITOR } from '../../notebook/common/notebookContext // settings export const enum InlineChatConfigKeys { - FinishOnType = 'inlineChat.finishOnType', /** @deprecated do not read on client */ EnableV2 = 'inlineChat.enableV2', - notebookAgent = 'inlineChat.notebookAgent', + NotebookAgent = 'inlineChat.notebookAgent', DefaultModel = 'inlineChat.defaultModel', Affordance = 'inlineChat.affordance', FixDiagnostics = 'inlineChat.fixDiagnostics', @@ -27,11 +26,6 @@ export const enum InlineChatConfigKeys { Registry.as(Extensions.Configuration).registerConfiguration({ id: 'editor', properties: { - [InlineChatConfigKeys.FinishOnType]: { - description: localize('finishOnType', "Whether to finish an inline chat session when typing outside of changed regions."), - default: false, - type: 'boolean' - }, [InlineChatConfigKeys.EnableV2]: { description: localize('enableV2', "Whether to use the next version of inline chat."), default: false, @@ -41,7 +35,7 @@ Registry.as(Extensions.Configuration).registerConfigurat mode: 'auto' } }, - [InlineChatConfigKeys.notebookAgent]: { + [InlineChatConfigKeys.NotebookAgent]: { markdownDescription: localize('notebookAgent', "Enable agent-like behavior for inline chat widget in notebooks."), default: false, type: 'boolean', diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index be4f138c4eaf1..6e6730635d576 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -18,16 +18,29 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IChatAcceptInputOptions, IChatWidgetService } from '../../../chat/browser/chat.js'; import { IChatAgentService } from '../../../chat/common/participants/chatAgents.js'; -import { IChatResponseModel, isCellTextEditOperationArray } from '../../../chat/common/model/chatModel.js'; +import { IChatModel, IChatResponseModel, isCellTextEditOperationArray } from '../../../chat/common/model/chatModel.js'; import { ChatMode } from '../../../chat/common/chatModes.js'; import { IChatModelReference, IChatProgress, IChatService } from '../../../chat/common/chatService/chatService.js'; import { ChatAgentLocation } from '../../../chat/common/constants.js'; -import { InlineChatWidget } from '../../../inlineChat/browser/inlineChatWidget.js'; +import { IInlineChatWidgetConstructionOptions, InlineChatWidget } from '../../../inlineChat/browser/inlineChatWidget.js'; import { MENU_INLINE_CHAT_WIDGET_SECONDARY } from '../../../inlineChat/common/inlineChat.js'; import { ITerminalInstance, type IXtermTerminal } from '../../../terminal/browser/terminal.js'; import { TerminalStickyScrollContribution } from '../../stickyScroll/browser/terminalStickyScrollContribution.js'; import './media/terminalChatWidget.css'; import { MENU_TERMINAL_CHAT_WIDGET_INPUT_SIDE_TOOLBAR, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from './terminalChat.js'; +import { ITextModel } from '../../../../../editor/common/model.js'; +import { isResponseVM } from '../../../chat/common/model/chatViewModel.js'; +import { IModelService } from '../../../../../editor/common/services/model.js'; +import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; +import { IAccessibleViewService } from '../../../../../platform/accessibility/browser/accessibleView.js'; +import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; +import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; +import { IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js'; +import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; +import { IChatWidgetLocationOptions } from '../../../chat/browser/widget/chatWidget.js'; +import { Selection } from '../../../../../editor/common/core/selection.js'; const enum Constants { HorizontalMargin = 10, @@ -58,8 +71,8 @@ export class TerminalChatWidget extends Disposable { private readonly _onDidHide = this._register(new Emitter()); readonly onDidHide = this._onDidHide.event; - private readonly _inlineChatWidget: InlineChatWidget; - public get inlineChatWidget(): InlineChatWidget { return this._inlineChatWidget; } + private readonly _inlineChatWidget: TerminalInlineChatWidget; + public get inlineChatWidget(): TerminalInlineChatWidget { return this._inlineChatWidget; } private readonly _focusTracker: IFocusTracker; @@ -116,7 +129,7 @@ export class TerminalChatWidget extends Disposable { this._terminalElement.appendChild(this._container); this._inlineChatWidget = instantiationService.createInstance( - InlineChatWidget, + TerminalInlineChatWidget, { location: ChatAgentLocation.Terminal, resolveData: () => { @@ -466,3 +479,120 @@ export class TerminalChatWidget extends Disposable { this.hide(); } } + + +class TerminalInlineChatWidget extends InlineChatWidget { + + + constructor( + location: IChatWidgetLocationOptions, + options: IInlineChatWidgetConstructionOptions, + @IInstantiationService instantiationService: IInstantiationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IKeybindingService keybindingService: IKeybindingService, + @IAccessibilityService accessibilityService: IAccessibilityService, + @IConfigurationService configurationService: IConfigurationService, + @IAccessibleViewService accessibleViewService: IAccessibleViewService, + @ITextModelService textModelResolverService: ITextModelService, + @IChatService chatService: IChatService, + @IHoverService hoverService: IHoverService, + @IChatEntitlementService chatEntitlementService: IChatEntitlementService, + @IMarkdownRendererService markdownRendererService: IMarkdownRendererService, + @IModelService private _modelService: IModelService, + ) { + super(location, options, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, textModelResolverService, chatService, hoverService, chatEntitlementService, markdownRendererService); + } + + get value(): string { + return this.chatWidget.getInput(); + } + + set value(value: string) { + this.chatWidget.setInput(value); + } + + selectAll() { + this.chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1)); + } + + set placeholder(value: string) { + this.chatWidget.setInputPlaceholder(value); + } + + toggleStatus(show: boolean) { + this._elements.toolbar1.classList.toggle('hidden', !show); + this._elements.toolbar2.classList.toggle('hidden', !show); + this._elements.status.classList.toggle('hidden', !show); + this._elements.infoLabel.classList.toggle('hidden', !show); + this._onDidChangeHeight.fire(); + } + + updateToolbar(show: boolean) { + this._elements.root.classList.toggle('toolbar', show); + this._elements.toolbar1.classList.toggle('hidden', !show); + this._elements.toolbar2.classList.toggle('hidden', !show); + this._elements.status.classList.toggle('actions', show); + this._elements.infoLabel.classList.toggle('hidden', show); + this._onDidChangeHeight.fire(); + } + + get responseContent(): string | undefined { + const requests = this.chatWidget.viewModel?.model.getRequests(); + return requests?.at(-1)?.response?.response.toString(); + } + + getChatModel(): IChatModel | undefined { + return this.chatWidget.viewModel?.model; + } + + setChatModel(chatModel: IChatModel) { + chatModel.inputModel.setState({ inputText: '', selections: [] }); + this.chatWidget.setModel(chatModel); + } + + async getCodeBlockInfo(codeBlockIndex: number): Promise { + const { viewModel } = this.chatWidget; + if (!viewModel) { + return undefined; + } + const items = viewModel.getItems().filter(i => isResponseVM(i)); + const item = items.at(-1); + if (!item) { + return; + } + + // Look for the code block in the rendered response + const codeBlocks = this.chatWidget.getCodeBlockInfosForResponse(item); + const info = codeBlocks[codeBlockIndex]; + if (info?.uri) { + return this._modelService.getModel(info.uri) ?? undefined; + } + + // Fallback: if the code block hasn't been rendered yet (e.g. due to + // timing between response completion and list rendering), parse the + // markdown directly and create a transient model. + const markdown = item.response.getMarkdown(); + let currentCodeBlockIndex = 0; + let foundText: string | undefined; + + for (const line of markdown.split('\n')) { + if (line.startsWith('```') && foundText === undefined) { + foundText = ''; + } else if (line.startsWith('```') && foundText !== undefined) { + if (currentCodeBlockIndex === codeBlockIndex) { + break; + } + currentCodeBlockIndex++; + foundText = undefined; + } else if (foundText !== undefined) { + foundText += (foundText ? '\n' : '') + line; + } + } + + if (foundText !== undefined && currentCodeBlockIndex === codeBlockIndex) { + return this._modelService.createModel(foundText, null, undefined, true); + } + + return undefined; + } +} diff --git a/src/vs/workbench/services/textfile/common/encoding.ts b/src/vs/workbench/services/textfile/common/encoding.ts index b119273a6ffa7..4e401ed6f98a2 100644 --- a/src/vs/workbench/services/textfile/common/encoding.ts +++ b/src/vs/workbench/services/textfile/common/encoding.ts @@ -693,85 +693,90 @@ export const SUPPORTED_ENCODINGS: EncodingsMap = { labelShort: 'ISO 8859-9', order: 33 }, + cp857: { + labelLong: 'Turkish (CP 857)', + labelShort: 'CP 857', + order: 34 + }, windows1258: { labelLong: 'Vietnamese (Windows 1258)', labelShort: 'Windows 1258', - order: 34 + order: 35 }, gbk: { labelLong: 'Simplified Chinese (GBK)', labelShort: 'GBK', - order: 35 + order: 36 }, gb18030: { labelLong: 'Simplified Chinese (GB18030)', labelShort: 'GB18030', - order: 36 + order: 37 }, cp950: { labelLong: 'Traditional Chinese (Big5)', labelShort: 'Big5', - order: 37, + order: 38, guessableName: 'Big5' }, big5hkscs: { labelLong: 'Traditional Chinese (Big5-HKSCS)', labelShort: 'Big5-HKSCS', - order: 38 + order: 39 }, shiftjis: { labelLong: 'Japanese (Shift JIS)', labelShort: 'Shift JIS', - order: 39, + order: 40, guessableName: 'SHIFT_JIS' }, eucjp: { labelLong: 'Japanese (EUC-JP)', labelShort: 'EUC-JP', - order: 40, + order: 41, guessableName: 'EUC-JP' }, euckr: { labelLong: 'Korean (EUC-KR)', labelShort: 'EUC-KR', - order: 41, + order: 42, guessableName: 'EUC-KR' }, windows874: { labelLong: 'Thai (Windows 874)', labelShort: 'Windows 874', - order: 42 + order: 43 }, iso885911: { labelLong: 'Latin/Thai (ISO 8859-11)', labelShort: 'ISO 8859-11', - order: 43 + order: 44 }, koi8ru: { labelLong: 'Cyrillic (KOI8-RU)', labelShort: 'KOI8-RU', - order: 44 + order: 45 }, koi8t: { labelLong: 'Tajik (KOI8-T)', labelShort: 'KOI8-T', - order: 45 + order: 46 }, gb2312: { labelLong: 'Simplified Chinese (GB 2312)', labelShort: 'GB 2312', - order: 46, + order: 47, guessableName: 'GB2312' }, cp865: { labelLong: 'Nordic DOS (CP 865)', labelShort: 'CP 865', - order: 47 + order: 48 }, cp850: { labelLong: 'Western European DOS (CP 850)', labelShort: 'CP 850', - order: 48 + order: 49 } }; diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 329826a2894cc..df78e006e8e0f 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -130,7 +130,7 @@ declare module 'vscode' { * 'iso88597', 'windows1255', 'iso88598', 'iso885910', 'iso885916', 'windows1254', * 'iso88599', 'windows1258', 'gbk', 'gb18030', 'cp950', 'big5hkscs', 'shiftjis', * 'eucjp', 'euckr', 'windows874', 'iso885911', 'koi8ru', 'koi8t', 'gb2312', - * 'cp865', 'cp850'. + * 'cp865', 'cp850', 'cp857'. */ readonly encoding: string;