diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index e82d2034bfd..a885606999e 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1751,6 +1751,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseReferencePart: extHostTypes.ChatResponseReferencePart, ChatResponseReferencePart2: extHostTypes.ChatResponseReferencePart, ChatResponseCodeCitationPart: extHostTypes.ChatResponseCodeCitationPart, + ChatResponseCodeblockUriPart: extHostTypes.ChatResponseCodeblockUriPart, ChatResponseWarningPart: extHostTypes.ChatResponseWarningPart, ChatResponseTextEditPart: extHostTypes.ChatResponseTextEditPart, ChatResponseMarkdownWithVulnerabilitiesPart: extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart, diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 5940c351d10..27ca9a8bbcc 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -121,6 +121,14 @@ class ChatAgentResponseStream { _report(dto); return this; }, + codeblockUri(value) { + throwIfDone(this.codeblockUri); + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + const part = new extHostTypes.ChatResponseCodeblockUriPart(value); + const dto = typeConvert.ChatResponseCodeblockUriPart.from(part); + _report(dto); + return this; + }, filetree(value, baseUri) { throwIfDone(this.filetree); const part = new extHostTypes.ChatResponseFileTreePart(value, baseUri); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 1abbe45add0..2f411ced17e 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -40,7 +40,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from '../../common/editor.js'; import { IViewBadge } from '../../common/views.js'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js'; import { IChatRequestVariableEntry } from '../../contrib/chat/common/chatModel.js'; -import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; +import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; import { IToolData, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import * as chatProvider from '../../contrib/chat/common/languageModels.js'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from '../../contrib/debug/common/debug.js'; @@ -2405,6 +2405,18 @@ export namespace ChatResponseMarkdownPart { } } +export namespace ChatResponseCodeblockUriPart { + export function from(part: vscode.ChatResponseCodeblockUriPart): Dto { + return { + kind: 'codeblockUri', + uri: part.value, + }; + } + export function to(part: Dto): vscode.ChatResponseCodeblockUriPart { + return new types.ChatResponseCodeblockUriPart(URI.revive(part.uri)); + } +} + export namespace ChatResponseMarkdownWithVulnerabilitiesPart { export function from(part: vscode.ChatResponseMarkdownWithVulnerabilitiesPart): Dto { return { @@ -2673,6 +2685,8 @@ export namespace ChatResponsePart { return ChatResponseTextEditPart.from(part); } else if (part instanceof types.ChatResponseMarkdownWithVulnerabilitiesPart) { return ChatResponseMarkdownWithVulnerabilitiesPart.from(part); + } else if (part instanceof types.ChatResponseCodeblockUriPart) { + return ChatResponseCodeblockUriPart.from(part); } else if (part instanceof types.ChatResponseDetectedParticipantPart) { return ChatResponseDetectedParticipantPart.from(part); } else if (part instanceof types.ChatResponseWarningPart) { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 58e0404d8ce..ada4e5e5156 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4477,6 +4477,13 @@ export class ChatResponseReferencePart { } } +export class ChatResponseCodeblockUriPart { + value: vscode.Uri; + constructor(value: vscode.Uri) { + this.value = value; + } +} + export class ChatResponseCodeCitationPart { value: vscode.Uri; license: string; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index fd64f264501..1c06c235045 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -3,10 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { coalesce } from '../../../../../base/common/arrays.js'; +import { AsyncIterableObject } from '../../../../../base/common/async.js'; import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { CharCode } from '../../../../../base/common/charCode.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; +import { ResourceMap } from '../../../../../base/common/map.js'; import { isEqual } from '../../../../../base/common/resources.js'; +import * as strings from '../../../../../base/common/strings.js'; +import { URI } from '../../../../../base/common/uri.js'; import { IActiveCodeEditor, ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { IBulkEditService, ResourceTextEdit } from '../../../../../editor/browser/services/bulkEditService.js'; @@ -28,26 +34,20 @@ import { INotificationService, Severity } from '../../../../../platform/notifica import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; import { TerminalLocation } from '../../../../../platform/terminal/common/terminal.js'; import { IUntitledTextResourceEditorInput } from '../../../../common/editor.js'; +import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { accessibleViewInCodeBlock } from '../../../accessibility/browser/accessibilityConfiguration.js'; -import { CHAT_CATEGORY } from './chatActions.js'; -import { IChatWidgetService, IChatCodeBlockContextProviderService } from '../chat.js'; -import { DefaultChatTextEditor, ICodeBlockActionContext, ICodeCompareBlockActionContext } from '../codeBlockPart.js'; -import { CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_EDIT_APPLIED } from '../../common/chatContextKeys.js'; -import { ChatCopyKind, IChatContentReference, IChatService, IDocumentContext } from '../../common/chatService.js'; -import { IChatResponseViewModel, isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; +import { InlineChatController } from '../../../inlineChat/browser/inlineChatController.js'; import { insertCell } from '../../../notebook/browser/controller/cellOperations.js'; import { INotebookEditor } from '../../../notebook/browser/notebookBrowser.js'; import { CellKind, NOTEBOOK_EDITOR_ID } from '../../../notebook/common/notebookCommon.js'; import { ITerminalEditorService, ITerminalGroupService, ITerminalService } from '../../../terminal/browser/terminal.js'; -import { IEditorService } from '../../../../services/editor/common/editorService.js'; -import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; -import * as strings from '../../../../../base/common/strings.js'; -import { CharCode } from '../../../../../base/common/charCode.js'; -import { InlineChatController } from '../../../inlineChat/browser/inlineChatController.js'; -import { coalesce } from '../../../../../base/common/arrays.js'; -import { AsyncIterableObject } from '../../../../../base/common/async.js'; -import { ResourceMap } from '../../../../../base/common/map.js'; -import { URI } from '../../../../../base/common/uri.js'; +import { CONTEXT_CHAT_EDIT_APPLIED, CONTEXT_CHAT_ENABLED, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION } from '../../common/chatContextKeys.js'; +import { ChatCopyKind, IChatContentReference, IChatService, IDocumentContext } from '../../common/chatService.js'; +import { IChatResponseViewModel, isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; +import { IChatCodeBlockContextProviderService, IChatWidgetService } from '../chat.js'; +import { DefaultChatTextEditor, ICodeBlockActionContext, ICodeCompareBlockActionContext } from '../codeBlockPart.js'; +import { CHAT_CATEGORY } from './chatActions.js'; const shellLangIds = [ 'fish', @@ -158,14 +158,26 @@ abstract class InsertCodeBlockAction extends ChatCodeBlockAction { override async runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext) { const editorService = accessor.get(IEditorService); const textFileService = accessor.get(ITextFileService); + const bulkEditService = accessor.get(IBulkEditService); + const codeEditorService = accessor.get(ICodeEditorService); + const chatService = accessor.get(IChatService); + const languageFeaturesService = accessor.get(ILanguageFeaturesService); + const notificationService = accessor.get(INotificationService); + const progressService = accessor.get(IProgressService); + const languageService = accessor.get(ILanguageService); if (isResponseFiltered(context)) { // When run from command palette return; } + if (context.codemapperUri) { + // If the code block is from a code mapper, first reveal the target file + await editorService.openEditor({ resource: context.codemapperUri }); + } + if (editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) { - return this.handleNotebookEditor(accessor, editorService.activeEditorPane.getControl() as INotebookEditor, context); + return this.handleNotebookEditor(languageService, progressService, notificationService, languageFeaturesService, bulkEditService, codeEditorService, chatService, editorService.activeEditorPane.getControl() as INotebookEditor, context); } let activeEditorControl = editorService.activeTextEditorControl; @@ -188,10 +200,10 @@ abstract class InsertCodeBlockAction extends ChatCodeBlockAction { return; } - await this.handleTextEditor(accessor, activeEditorControl, context); + await this.handleTextEditor(progressService, notificationService, languageFeaturesService, bulkEditService, codeEditorService, chatService, activeEditorControl, context); } - private async handleNotebookEditor(accessor: ServicesAccessor, notebookEditor: INotebookEditor, context: ICodeBlockActionContext) { + private async handleNotebookEditor(languageService: ILanguageService, progressService: IProgressService, notificationService: INotificationService, languageFeaturesService: ILanguageFeaturesService, bulkEditService: IBulkEditService, codeEditorService: ICodeEditorService, chatService: IChatService, notebookEditor: INotebookEditor, context: ICodeBlockActionContext) { if (!notebookEditor.hasModel()) { return; } @@ -203,20 +215,17 @@ abstract class InsertCodeBlockAction extends ChatCodeBlockAction { if (notebookEditor.activeCodeEditor?.hasTextFocus()) { const codeEditor = notebookEditor.activeCodeEditor; if (codeEditor.hasModel()) { - return this.handleTextEditor(accessor, codeEditor, context); + return this.handleTextEditor(progressService, notificationService, languageFeaturesService, bulkEditService, codeEditorService, chatService, codeEditor, context); } } - const languageService = accessor.get(ILanguageService); - const chatService = accessor.get(IChatService); - const focusRange = notebookEditor.getFocus(); const next = Math.max(focusRange.end - 1, 0); insertCell(languageService, notebookEditor, next, CellKind.Code, 'below', context.code, true); this.notifyUserAction(chatService, context); } - protected async computeEdits(accessor: ServicesAccessor, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { + protected async computeEdits(progressService: IProgressService, notificationService: INotificationService, languageFeaturesService: ILanguageFeaturesService, bulkEditService: IBulkEditService, codeEditorService: ICodeEditorService, chatService: IChatService, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { const activeModel = codeEditor.getModel(); const range = codeEditor.getSelection() ?? new Range(activeModel.getLineCount(), 1, activeModel.getLineCount(), 1); const text = reindent(codeBlockActionContext.code, activeModel, range.startLineNumber); @@ -230,12 +239,8 @@ abstract class InsertCodeBlockAction extends ChatCodeBlockAction { return false; } - private async handleTextEditor(accessor: ServicesAccessor, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext) { - const bulkEditService = accessor.get(IBulkEditService); - const codeEditorService = accessor.get(ICodeEditorService); - const chatService = accessor.get(IChatService); - - const result = await this.computeEdits(accessor, codeEditor, codeBlockActionContext); + private async handleTextEditor(progressService: IProgressService, notificationService: INotificationService, languageFeaturesService: ILanguageFeaturesService, bulkEditService: IBulkEditService, codeEditorService: ICodeEditorService, chatService: IChatService, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext) { + const result = await this.computeEdits(progressService, notificationService, languageFeaturesService, bulkEditService, codeEditorService, chatService, codeEditor, codeBlockActionContext); this.notifyUserAction(chatService, codeBlockActionContext, result); if (!result) { return; @@ -489,14 +494,12 @@ export function registerChatCodeBlockActions() { }); } - protected override async computeEdits(accessor: ServicesAccessor, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { + protected override async computeEdits(progressService: IProgressService, notificationService: INotificationService, languageFeaturesService: ILanguageFeaturesService, bulkEditService: IBulkEditService, codeEditorService: ICodeEditorService, chatService: IChatService, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { - const progressService = accessor.get(IProgressService); - const notificationService = accessor.get(INotificationService); const activeModel = codeEditor.getModel(); - const mappedEditsProviders = accessor.get(ILanguageFeaturesService).mappedEditsProvider.ordered(activeModel); + const mappedEditsProviders = languageFeaturesService.mappedEditsProvider.ordered(activeModel); if (mappedEditsProviders.length > 0) { // 0th sub-array - editor selections array if there are any selections @@ -819,6 +822,7 @@ function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): codeBlockIndex: codeBlockInfo.codeBlockIndex, code: editor.getValue(), languageId: editor.getModel()!.getLanguageId(), + codemapperUri: codeBlockInfo.codemapperUri }; } diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index fc3af5c6ee6..6273a5e0310 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -80,6 +80,7 @@ export interface IChatCodeBlockInfo { codeBlockIndex: number; element: ChatTreeItem; uri: URI | undefined; + codemapperUri: URI | undefined; focus(): void; } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index e6184f510ae..da42f3e51d3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -25,6 +25,7 @@ import { IMarkdownVulnerability } from '../../common/annotations.js'; import { IChatProgressRenderableResponseContent } from '../../common/chatModel.js'; import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; import { CodeBlockModelCollection } from '../../common/codeBlockModelCollection.js'; +import { URI } from '../../../../../base/common/uri.js'; const $ = dom.$; @@ -66,6 +67,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP let textModel: Promise; let range: Range | undefined; let vulns: readonly IMarkdownVulnerability[] | undefined; + let codemapperUri: URI | undefined; if (equalsIgnoreCase(languageId, localFileLanguageId)) { try { const parsedBody = parseLocalFileData(text); @@ -83,11 +85,12 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP const sessionId = isResponseVM(element) || isRequestVM(element) ? element.sessionId : ''; const modelEntry = this.codeBlockModelCollection.getOrCreate(sessionId, element, index); vulns = modelEntry.vulns; + codemapperUri = modelEntry.codemapperUri; textModel = modelEntry.model; } const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered; - const ref = this.renderCodeBlock({ languageId, textModel, codeBlockIndex: index, element, range, hideToolbar, parentContextKeyService: contextKeyService, vulns }, text, currentWidth, rendererOptions.editableCodeBlock); + const ref = this.renderCodeBlock({ languageId, textModel, codeBlockIndex: index, element, range, hideToolbar, parentContextKeyService: contextKeyService, vulns, codemapperUri }, text, currentWidth, rendererOptions.editableCodeBlock); this.allRefs.push(ref); // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) @@ -100,7 +103,8 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP focus() { ref.object.focus(); }, - uri: ref.object.uri + uri: ref.object.uri, + codemapperUri: undefined }; this.codeblocks.push(info); orderedDisposablesList.push(ref); @@ -119,7 +123,9 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP const ref = this.editorPool.get(); const editorInfo = ref.object; if (isResponseVM(data.element)) { - this.codeBlockModelCollection.update(data.element.sessionId, data.element, data.codeBlockIndex, { text, languageId: data.languageId }); + this.codeBlockModelCollection.update(data.element.sessionId, data.element, data.codeBlockIndex, { text, languageId: data.languageId }).then((e) => { + this.codeblocks[data.codeBlockIndex] = { ...this.codeblocks[data.codeBlockIndex]!, codemapperUri: e.codemapperUri }; + }); } editorInfo.render(data, currentWidth, editableCodeBlock); diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index b83e09cd7d4..9fdb65576e6 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -79,6 +79,8 @@ export interface ICodeBlockData { readonly textModel: Promise; readonly languageId: string; + readonly codemapperUri?: URI; + readonly vulns?: readonly IMarkdownVulnerability[]; readonly range?: Range; @@ -126,6 +128,7 @@ export function parseLocalFileData(text: string) { export interface ICodeBlockActionContext { code: string; + codemapperUri?: URI; languageId?: string; codeBlockIndex: number; element: unknown; @@ -424,7 +427,8 @@ export class CodeBlockPart extends Disposable { code: textModel.getTextBuffer().getValueInRange(data.range ?? textModel.getFullModelRange(), EndOfLinePreference.TextDefined), codeBlockIndex: data.codeBlockIndex, element: data.element, - languageId: textModel.getLanguageId() + languageId: textModel.getLanguageId(), + codemapperUri: data.codemapperUri, } satisfies ICodeBlockActionContext; this.resourceContextKey.set(textModel.uri); } diff --git a/src/vs/workbench/contrib/chat/common/annotations.ts b/src/vs/workbench/contrib/chat/common/annotations.ts index 5d0fd7db153..bd95ecb599e 100644 --- a/src/vs/workbench/contrib/chat/common/annotations.ts +++ b/src/vs/workbench/contrib/chat/common/annotations.ts @@ -61,6 +61,12 @@ export function annotateSpecialMarkdownContent(response: ReadonlyArray${item.uri.toString()}`; + const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText)); + result[result.length - 1] = { content: merged, kind: 'markdownContent' }; + } } else { result.push(item); } @@ -99,6 +105,16 @@ export function annotateVulnerabilitiesInText(response: ReadonlyArray(.*?)<\/vscode_codeblock_uri>/ms.exec(text); + if (match && match[1]) { + const result = URI.parse(match[1]); + const textWithoutResult = text.substring(0, match.index) + text.substring(match.index + match[0].length); + return { uri: result, textWithoutResult }; + } + return undefined; +} + export function extractVulnerabilitiesFromText(text: string): { newText: string; vulnerabilities: IMarkdownVulnerability[] } { const vulnerabilities: IMarkdownVulnerability[] = []; let newText = text; diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 30100ee1940..515156496d7 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -22,7 +22,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, reviveSerializedAgent } from './chatAgents.js'; import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from './chatParserTypes.js'; -import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; import { IChatRequestVariableValue } from './chatVariables.js'; export interface IChatRequestVariableEntry { @@ -74,6 +74,7 @@ export interface IChatTextEditGroup { export type IChatProgressResponseContent = | IChatMarkdownContent | IChatAgentMarkdownContentWithVulnerability + | IChatResponseCodeblockUriPart | IChatTreeData | IChatContentInlineReference | IChatProgressMessage @@ -83,7 +84,7 @@ export type IChatProgressResponseContent = | IChatTextEditGroup | IChatConfirmation; -export type IChatProgressRenderableResponseContent = Exclude; +export type IChatProgressRenderableResponseContent = Exclude; export interface IResponse { readonly value: ReadonlyArray; @@ -203,7 +204,7 @@ export class Response extends Disposable implements IResponse { return this._responseParts; } - constructor(value: IMarkdownString | ReadonlyArray) { + constructor(value: IMarkdownString | ReadonlyArray) { super(); this._responseParts = asArray(value).map((v) => (isMarkdownString(v) ? { content: v, kind: 'markdownContent' } satisfies IChatMarkdownContent : @@ -301,7 +302,7 @@ export class Response extends Disposable implements IResponse { return part.command.title; } else if (part.kind === 'textEditGroup') { return localize('editsSummary', "Made changes."); - } else if (part.kind === 'progressMessage') { + } else if (part.kind === 'progressMessage' || part.kind === 'codeblockUri') { return ''; } else if (part.kind === 'confirmation') { return `${part.title}\n${part.message}`; @@ -422,7 +423,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel } constructor( - _response: IMarkdownString | ReadonlyArray, + _response: IMarkdownString | ReadonlyArray, private _session: ChatModel, private _agent: IChatAgentData | undefined, private _slashCommand: IChatAgentCommand | undefined, @@ -1047,6 +1048,7 @@ export class ChatModel extends Disposable implements IChatModel { if (progress.kind === 'markdownContent' || progress.kind === 'treeData' || progress.kind === 'inlineReference' || + progress.kind === 'codeblockUri' || progress.kind === 'markdownVuln' || progress.kind === 'progressMessage' || progress.kind === 'command' || diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 99edf364e6f..25804510aca 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -154,6 +154,11 @@ export interface IChatAgentVulnerabilityDetails { description: string; } +export interface IChatResponseCodeblockUriPart { + kind: 'codeblockUri'; + uri: URI; +} + export interface IChatAgentMarkdownContentWithVulnerability { content: IMarkdownString; vulnerabilities: IChatAgentVulnerabilityDetails[]; @@ -202,6 +207,7 @@ export type IChatProgress = | IChatWarningMessage | IChatTextEdit | IChatMoveMessage + | IChatResponseCodeblockUriPart | IChatConfirmation; export interface IChatFollowup { diff --git a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts index 92a93ae015a..ad67a6ccc24 100644 --- a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts +++ b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts @@ -11,8 +11,8 @@ import { Range } from '../../../../editor/common/core/range.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { EndOfLinePreference } from '../../../../editor/common/model.js'; import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js'; +import { extractCodeblockUrisFromText, extractVulnerabilitiesFromText, IMarkdownVulnerability } from './annotations.js'; import { IChatRequestViewModel, IChatResponseViewModel, isResponseVM } from './chatViewModel.js'; -import { extractVulnerabilitiesFromText, IMarkdownVulnerability } from './annotations.js'; export class CodeBlockModelCollection extends Disposable { @@ -20,6 +20,7 @@ export class CodeBlockModelCollection extends Disposable { private readonly _models = new ResourceMap<{ readonly model: Promise>; vulns: readonly IMarkdownVulnerability[]; + codemapperUri?: URI; }>(); /** @@ -41,16 +42,16 @@ export class CodeBlockModelCollection extends Disposable { this.clear(); } - get(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): { model: Promise; readonly vulns: readonly IMarkdownVulnerability[] } | undefined { + get(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): { model: Promise; readonly vulns: readonly IMarkdownVulnerability[]; readonly codemapperUri?: URI } | undefined { const uri = this.getUri(sessionId, chat, codeBlockIndex); const entry = this._models.get(uri); if (!entry) { return; } - return { model: entry.model.then(ref => ref.object), vulns: entry.vulns }; + return { model: entry.model.then(ref => ref.object), vulns: entry.vulns, codemapperUri: entry.codemapperUri }; } - getOrCreate(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): { model: Promise; readonly vulns: readonly IMarkdownVulnerability[] } { + getOrCreate(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): { model: Promise; readonly vulns: readonly IMarkdownVulnerability[]; readonly codemapperUri?: URI } { const existing = this.get(sessionId, chat, codeBlockIndex); if (existing) { return existing; @@ -58,7 +59,7 @@ export class CodeBlockModelCollection extends Disposable { const uri = this.getUri(sessionId, chat, codeBlockIndex); const ref = this.textModelService.createModelReference(uri); - this._models.set(uri, { model: ref, vulns: [] }); + this._models.set(uri, { model: ref, vulns: [], codemapperUri: undefined }); while (this._models.size > this.maxModelCount) { const first = Array.from(this._models.keys()).at(0); @@ -68,7 +69,7 @@ export class CodeBlockModelCollection extends Disposable { this.delete(first); } - return { model: ref.then(ref => ref.object), vulns: [] }; + return { model: ref.then(ref => ref.object), vulns: [], codemapperUri: undefined }; } private delete(codeBlockUri: URI) { @@ -90,9 +91,15 @@ export class CodeBlockModelCollection extends Disposable { const entry = this.getOrCreate(sessionId, chat, codeBlockIndex); const extractedVulns = extractVulnerabilitiesFromText(content.text); - const newText = fixCodeText(extractedVulns.newText, content.languageId); + let newText = fixCodeText(extractedVulns.newText, content.languageId); this.setVulns(sessionId, chat, codeBlockIndex, extractedVulns.vulnerabilities); + const codeblockUri = extractCodeblockUrisFromText(newText); + if (codeblockUri) { + this.setCodemapperUri(sessionId, chat, codeBlockIndex, codeblockUri.uri); + newText = codeblockUri.textWithoutResult; + } + const textModel = (await entry.model).textEditorModel; if (content.languageId) { const vscodeLanguageId = this.languageService.getLanguageIdByLanguageName(content.languageId); @@ -103,7 +110,7 @@ export class CodeBlockModelCollection extends Disposable { const currentText = textModel.getValue(EndOfLinePreference.LF); if (newText === currentText) { - return; + return entry; } if (newText.startsWith(currentText)) { @@ -115,6 +122,16 @@ export class CodeBlockModelCollection extends Disposable { // console.log(`Failed to optimize setText`); textModel.setValue(newText); } + + return entry; + } + + private setCodemapperUri(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, codemapperUri: URI) { + const uri = this.getUri(sessionId, chat, codeBlockIndex); + const entry = this._models.get(uri); + if (entry) { + entry.codemapperUri = codemapperUri; + } } private setVulns(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, vulnerabilities: IMarkdownVulnerability[]) { diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 9352503656d..ab40805bb9b 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -36,6 +36,11 @@ declare module 'vscode' { constructor(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]); } + export class ChatResponseCodeblockUriPart { + value: Uri; + constructor(value: Uri); + } + /** * Displays a {@link Command command} as a button in the chat response. */ @@ -155,6 +160,7 @@ declare module 'vscode' { textEdit(target: Uri, edits: TextEdit | TextEdit[]): void; markdownWithVulnerabilities(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]): void; + codeblockUri(uri: Uri): void; detectedParticipant(participant: string, command?: ChatCommand): void; push(part: ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseWarningPart | ChatResponseProgressPart2): void;