diff --git a/src/vs/editor/common/core/edits/lineEdit.ts b/src/vs/editor/common/core/edits/lineEdit.ts index beccdc1fc05..83d684da8e0 100644 --- a/src/vs/editor/common/core/edits/lineEdit.ts +++ b/src/vs/editor/common/core/edits/lineEdit.ts @@ -7,7 +7,7 @@ import { compareBy, groupAdjacentBy, numberComparator } from '../../../../base/c import { assert, checkAdjacentItems } from '../../../../base/common/assert.js'; import { splitLines } from '../../../../base/common/strings.js'; import { LineRange } from '../ranges/lineRange.js'; -import { StringEdit, StringReplacement } from './stringEdit.js'; +import { BaseStringEdit, StringEdit, StringReplacement } from './stringEdit.js'; import { Position } from '../position.js'; import { Range } from '../range.js'; import { TextReplacement, TextEdit } from './textEdit.js'; @@ -20,7 +20,7 @@ export class LineEdit { return new LineEdit(data.map(e => LineReplacement.deserialize(e))); } - public static fromEdit(edit: StringEdit, initialValue: AbstractText): LineEdit { + public static fromEdit(edit: BaseStringEdit, initialValue: AbstractText): LineEdit { const textEdit = TextEdit.fromStringEdit(edit, initialValue); return LineEdit.fromTextEdit(textEdit, initialValue); } diff --git a/src/vs/editor/common/core/edits/textEdit.ts b/src/vs/editor/common/core/edits/textEdit.ts index f12c562a242..4a1472d224f 100644 --- a/src/vs/editor/common/core/edits/textEdit.ts +++ b/src/vs/editor/common/core/edits/textEdit.ts @@ -8,14 +8,14 @@ import { assertFn, checkAdjacentItems } from '../../../../base/common/assert.js' import { BugIndicatingError } from '../../../../base/common/errors.js'; import { commonPrefixLength, commonSuffixLength } from '../../../../base/common/strings.js'; import { ISingleEditOperation } from '../editOperation.js'; -import { StringEdit, StringReplacement } from './stringEdit.js'; +import { BaseStringEdit, StringReplacement } from './stringEdit.js'; import { Position } from '../position.js'; import { Range } from '../range.js'; import { TextLength } from '../text/textLength.js'; import { AbstractText, StringText } from '../text/abstractText.js'; export class TextEdit { - public static fromStringEdit(edit: StringEdit, initialState: AbstractText): TextEdit { + public static fromStringEdit(edit: BaseStringEdit, initialState: AbstractText): TextEdit { const edits = edit.replacements.map(e => TextReplacement.fromStringReplacement(e, initialState)); return new TextEdit(edits); } diff --git a/src/vs/editor/common/textModelEditSource.ts b/src/vs/editor/common/textModelEditSource.ts index fb49c7adc2c..277d6b13d4b 100644 --- a/src/vs/editor/common/textModelEditSource.ts +++ b/src/vs/editor/common/textModelEditSource.ts @@ -3,6 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { sumBy } from '../../base/common/arrays.js'; +import { generateUuid } from '../../base/common/uuid.js'; +import { LineEdit } from './core/edits/lineEdit.js'; +import { BaseStringEdit } from './core/edits/stringEdit.js'; +import { StringText } from './core/text/abstractText.js'; +import { TextLength } from './core/text/textLength.js'; import { ProviderId, VersionedExtensionId } from './languages.js'; const privateSymbol = Symbol('TextModelEditSource'); @@ -91,7 +97,15 @@ export const EditSources = { rename: () => createEditSource({ source: 'rename' } as const), - chatApplyEdits(data: { modelId: string | undefined; sessionId: string | undefined; requestId: string | undefined; languageId: string; mode: string | undefined; extensionId: VersionedExtensionId | undefined }) { + chatApplyEdits(data: { + modelId: string | undefined; + sessionId: string | undefined; + requestId: string | undefined; + languageId: string; + mode: string | undefined; + extensionId: VersionedExtensionId | undefined; + codeBlockSuggestionId: EditSuggestionId | undefined; + }) { return createEditSource({ source: 'Chat.applyEdits', $modelId: avoidPathRedaction(data.modelId), @@ -101,6 +115,7 @@ export const EditSources = { $$sessionId: data.sessionId, $$requestId: data.requestId, $$mode: data.mode, + $$codeBlockSuggestionId: data.codeBlockSuggestionId, } as const); }, @@ -181,3 +196,62 @@ function avoidPathRedaction(str: string | undefined): string | undefined { // To avoid false-positive file path redaction. return str.replaceAll('/', '|'); } + + +export class EditDeltaInfo { + public static fromText(text: string): EditDeltaInfo { + const linesAdded = TextLength.ofText(text).lineCount; + const charsAdded = text.length; + return new EditDeltaInfo(linesAdded, 0, charsAdded, 0); + } + + public static fromEdit(edit: BaseStringEdit, originalString: StringText): EditDeltaInfo { + const lineEdit = LineEdit.fromEdit(edit, originalString); + const linesAdded = sumBy(lineEdit.replacements, r => r.newLines.length); + const linesRemoved = sumBy(lineEdit.replacements, r => r.lineRange.length); + const charsAdded = sumBy(edit.replacements, r => r.getNewLength()); + const charsRemoved = sumBy(edit.replacements, r => r.replaceRange.length); + return new EditDeltaInfo(linesAdded, linesRemoved, charsAdded, charsRemoved); + } + + public static tryCreate( + linesAdded: number | undefined, + linesRemoved: number | undefined, + charsAdded: number | undefined, + charsRemoved: number | undefined + ): EditDeltaInfo | undefined { + if (linesAdded === undefined || linesRemoved === undefined || charsAdded === undefined || charsRemoved === undefined) { + return undefined; + } + return new EditDeltaInfo(linesAdded, linesRemoved, charsAdded, charsRemoved); + } + + constructor( + public readonly linesAdded: number, + public readonly linesRemoved: number, + public readonly charsAdded: number, + public readonly charsRemoved: number + ) { } +} + + +/** + * This is an opaque serializable type that represents a unique identity for an edit. + */ +export interface EditSuggestionId { + readonly _brand: 'EditIdentity'; +} + +export namespace EditSuggestionId { + /** + * Use AiEditTelemetryServiceImpl to create a new id! + */ + export function newId(): EditSuggestionId { + const id = generateUuid(); + return toEditIdentity(id); + } +} + +function toEditIdentity(id: string): EditSuggestionId { + return id as unknown as EditSuggestionId; +} diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index ab5f12e2bf4..7ab4762483c 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -37,6 +37,8 @@ import { ExtHostContext, ExtHostLanguageFeaturesShape, HoverWithId, ICallHierarc import { InlineCompletionEndOfLifeReasonKind } from '../common/extHostTypes.js'; import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; import { DataChannelForwardingTelemetryService, forwardToChannelIf, isCopilotLikeExtension } from '../../contrib/editTelemetry/browser/telemetry/forwardingTelemetryService.js'; +import { IAiEditTelemetryService } from '../../contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js'; +import { EditDeltaInfo } from '../../../editor/common/textModelEditSource.js'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures extends Disposable implements MainThreadLanguageFeaturesShape { @@ -622,9 +624,23 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread $registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[], supportsHandleEvents: boolean, extensionId: string, extensionVersion: string, groupId: string | undefined, yieldsToExtensionIds: string[], displayName: string | undefined, debounceDelayMs: number | undefined, excludesExtensionIds: string[], eventHandle: number | undefined): void { const provider: languages.InlineCompletionsProvider = { provideInlineCompletions: async (model: ITextModel, position: EditorPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise => { - return this._proxy.$provideInlineCompletions(handle, model.uri, position, context, token); + const result = await this._proxy.$provideInlineCompletions(handle, model.uri, position, context, token); + return result; }, handleItemDidShow: async (completions: IdentifiableInlineCompletions, item: IdentifiableInlineCompletion, updatedInsertText: string): Promise => { + this._instantiationService.invokeFunction(accessor => { + const aiEditTelemetryService = accessor.getIfExists(IAiEditTelemetryService); + item.suggestionId = aiEditTelemetryService?.createSuggestionId({ + applyCodeBlockSuggestionId: undefined, + editDeltaInfo: new EditDeltaInfo(1, 1, -1, -1), // TODO@hediet, fix this approximation. + feature: 'inlineSuggestion', + languageId: completions.languageId, + modeId: undefined, + modelId: undefined, + presentation: 'inlineSuggestion', + }); + }); + if (supportsHandleEvents) { await this._proxy.$handleInlineCompletionDidShow(handle, completions.pid, item.idx, updatedInsertText); } @@ -649,6 +665,29 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread if (supportsHandleEvents) { await this._proxy.$handleInlineCompletionEndOfLifetime(handle, completions.pid, item.idx, mapReason(reason, i => ({ pid: completions.pid, idx: i.idx }))); } + + if (reason.kind === languages.InlineCompletionEndOfLifeReasonKind.Accepted) { + this._instantiationService.invokeFunction(accessor => { + const aiEditTelemetryService = accessor.getIfExists(IAiEditTelemetryService); + aiEditTelemetryService?.handleCodeAccepted({ + suggestionId: item.suggestionId, + editDeltaInfo: EditDeltaInfo.tryCreate( + lifetimeSummary.lineCountModified, + lifetimeSummary.lineCountOriginal, + lifetimeSummary.characterCountModified, + lifetimeSummary.characterCountOriginal, + ), + feature: 'inlineSuggestion', + languageId: completions.languageId, + modeId: undefined, + modelId: undefined, + presentation: 'inlineSuggestion', + acceptanceMethod: 'accept', + applyCodeBlockSuggestionId: undefined, + }); + }); + } + const endOfLifeSummary: InlineCompletionEndOfLifeEvent = { id: lifetimeSummary.requestUuid, opportunityId: lifetimeSummary.requestUuid, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 4d1d539cc13..090aaaa1c05 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -28,6 +28,7 @@ import * as languages from '../../../editor/common/languages.js'; import { CompletionItemLabel } from '../../../editor/common/languages.js'; import { CharacterPair, CommentRule, EnterAction } from '../../../editor/common/languages/languageConfiguration.js'; import { EndOfLineSequence } from '../../../editor/common/model.js'; +import { EditSuggestionId } from '../../../editor/common/textModelEditSource.js'; import { ISerializedModelContentChangedEvent } from '../../../editor/common/textModelEvents.js'; import { IAccessibilityInformation } from '../../../platform/accessibility/common/accessibility.js'; import { ILocalizedString } from '../../../platform/action/common/action.js'; @@ -463,10 +464,12 @@ export interface ISignatureHelpProviderMetadataDto { export interface IdentifiableInlineCompletions extends languages.InlineCompletions { pid: number; + languageId: string; } export interface IdentifiableInlineCompletion extends languages.InlineCompletion { idx: number; + suggestionId: EditSuggestionId | undefined; } export interface MainThreadLanguageFeaturesShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index d31b911cca5..b853010113d 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1399,6 +1399,7 @@ class InlineCompletionAdapter { return { pid, + languageId: doc.languageId, items: resultItems.map((item, idx) => { let command: languages.Command | undefined = undefined; if (item.command) { @@ -1438,6 +1439,7 @@ class InlineCompletionAdapter { icon: item.warning.icon ? typeConvert.IconPath.fromThemeIcon(item.warning.icon) : undefined, } : undefined, correlationId: this._isAdditionsProposedApiEnabled ? item.correlationId : undefined, + suggestionId: undefined, }); }), commands: commands.map(c => { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index ef21a3d4026..03c658386af 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -27,6 +27,8 @@ import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { IUntitledTextResourceEditorInput } from '../../../../common/editor.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; 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 { ITerminalEditorService, ITerminalGroupService, ITerminalService } from '../../../terminal/browser/terminal.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; @@ -148,6 +150,7 @@ export function registerChatCodeBlockActions() { } const clipboardService = accessor.get(IClipboardService); + const aiEditTelemetryService = accessor.get(IAiEditTelemetryService); clipboardService.writeText(context.code); if (isResponseVM(context.element)) { @@ -173,6 +176,19 @@ export function registerChatCodeBlockActions() { modelId: request?.modelId ?? '' } }); + + const codeBlockInfo = context.element.model.codeBlockInfos?.at(context.codeBlockIndex); + aiEditTelemetryService.handleCodeAccepted({ + acceptanceMethod: 'copyButton', + suggestionId: codeBlockInfo?.suggestionId, + editDeltaInfo: EditDeltaInfo.fromText(context.code), + feature: 'sideBarChat', + languageId: context.languageId, + modeId: context.element.model.request?.modeInfo?.modeId, + modelId: request?.modelId, + presentation: 'codeBlock', + applyCodeBlockSuggestionId: undefined, + }); } } }); @@ -202,6 +218,7 @@ export function registerChatCodeBlockActions() { // Report copy to extensions const chatService = accessor.get(IChatService); + const aiEditTelemetryService = accessor.get(IAiEditTelemetryService); const element = context.element as IChatResponseViewModel | undefined; if (isResponseVM(element)) { const requestId = element.requestId; @@ -225,6 +242,19 @@ export function registerChatCodeBlockActions() { modelId: request?.modelId ?? '' } }); + + const codeBlockInfo = element.model.codeBlockInfos?.at(context.codeBlockIndex); + aiEditTelemetryService.handleCodeAccepted({ + acceptanceMethod: 'copyManual', + suggestionId: codeBlockInfo?.suggestionId, + editDeltaInfo: EditDeltaInfo.fromText(copiedText), + feature: 'sideBarChat', + languageId: context.languageId, + modeId: element.model.request?.modeInfo?.modeId, + modelId: request?.modelId, + presentation: 'codeBlock', + applyCodeBlockSuggestionId: undefined, + }); } // Copy full cell if no selection, otherwise fall back on normal editor implementation @@ -344,6 +374,7 @@ export function registerChatCodeBlockActions() { const editorService = accessor.get(IEditorService); const chatService = accessor.get(IChatService); + const aiEditTelemetryService = accessor.get(IAiEditTelemetryService); editorService.openEditor({ contents: context.code, languageId: context.languageId, resource: undefined } satisfies IUntitledTextResourceEditorInput); @@ -366,6 +397,20 @@ export function registerChatCodeBlockActions() { modelId: request?.modelId ?? '' } }); + + const codeBlockInfo = context.element.model.codeBlockInfos?.at(context.codeBlockIndex); + + aiEditTelemetryService.handleCodeAccepted({ + acceptanceMethod: 'insertInNewFile', + suggestionId: codeBlockInfo?.suggestionId, + editDeltaInfo: EditDeltaInfo.fromText(context.code), + feature: 'sideBarChat', + languageId: context.languageId, + modeId: context.element.model.request?.modeInfo?.modeId, + modelId: request?.modelId, + presentation: 'codeBlock', + applyCodeBlockSuggestionId: undefined, + }); } } }); @@ -620,7 +665,7 @@ export function registerChatCodeCompareBlockActions() { const editorToApply = await editorService.openCodeEditor({ resource: item.uri }, null); if (editorToApply) { editorToApply.revealLineInCenterIfOutsideViewport(firstEdit.range.startLineNumber); - instaService.invokeFunction(reviewEdits, editorToApply, textEdits, CancellationToken.None); + instaService.invokeFunction(reviewEdits, editorToApply, textEdits, CancellationToken.None, undefined); response.setEditApplied(item, 1); return true; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index b8d78eff163..9dfdbd1ae13 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -17,25 +17,27 @@ import { Range } from '../../../../../editor/common/core/range.js'; import { TextEdit } from '../../../../../editor/common/languages.js'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; import { ITextModel } from '../../../../../editor/common/model.js'; +import { EditDeltaInfo, EditSuggestionId } from '../../../../../editor/common/textModelEditSource.js'; import { localize } from '../../../../../nls.js'; import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; +import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; 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 { 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'; +import { INotebookService } from '../../../notebook/common/notebookService.js'; import { ICodeMapperCodeBlock, ICodeMapperRequest, ICodeMapperResponse, ICodeMapperService } from '../../common/chatCodeMapperService.js'; import { ChatUserAction, IChatService } from '../../common/chatService.js'; import { IChatRequestViewModel, isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; import { ICodeBlockActionContext } from '../codeBlockPart.js'; -import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; -import { ILabelService } from '../../../../../platform/label/common/label.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { INotebookService } from '../../../notebook/common/notebookService.js'; export class InsertCodeBlockOperation { constructor( @@ -46,6 +48,7 @@ export class InsertCodeBlockOperation { @IChatService private readonly chatService: IChatService, @ILanguageService private readonly languageService: ILanguageService, @IDialogService private readonly dialogService: IDialogService, + @IAiEditTelemetryService private readonly aiEditTelemetryService: IAiEditTelemetryService, ) { } @@ -73,6 +76,20 @@ export class InsertCodeBlockOperation { languageId: context.languageId, modelId: request?.modelId ?? '', }); + + const codeBlockInfo = context.element.model.codeBlockInfos?.at(context.codeBlockIndex); + + this.aiEditTelemetryService.handleCodeAccepted({ + acceptanceMethod: 'insertAtCursor', + suggestionId: codeBlockInfo?.suggestionId, + editDeltaInfo: EditDeltaInfo.fromText(context.code), + feature: 'sideBarChat', + languageId: context.languageId, + modeId: context.element.model.request?.modeInfo?.modeId, + modelId: request?.modelId, + presentation: 'codeBlock', + applyCodeBlockSuggestionId: undefined, + }); } } @@ -155,10 +172,19 @@ export class ApplyCodeBlockOperation { } } + let codeBlockSuggestionId: EditSuggestionId | undefined = undefined; + + if (isResponseVM(context.element)) { + const codeBlockInfo = context.element.model.codeBlockInfos?.at(context.codeBlockIndex); + if (codeBlockInfo) { + codeBlockSuggestionId = codeBlockInfo.suggestionId; + } + } + let result: IComputeEditsResult | undefined = undefined; if (activeEditorControl && !this.notebookService.hasSupportedNotebooks(codemapperUri)) { - result = await this.handleTextEditor(activeEditorControl, context.chatSessionId, context.code); + result = await this.handleTextEditor(activeEditorControl, context.chatSessionId, context.code, codeBlockSuggestionId); } else { const activeNotebookEditor = getActiveNotebookEditor(this.editorService); if (activeNotebookEditor) { @@ -269,7 +295,7 @@ export class ApplyCodeBlockOperation { }; } - private async handleTextEditor(codeEditor: IActiveCodeEditor, chatSessionId: string | undefined, code: string): Promise { + private async handleTextEditor(codeEditor: IActiveCodeEditor, chatSessionId: string | undefined, code: string, applyCodeBlockSuggestionId: EditSuggestionId | undefined): Promise { const activeModel = codeEditor.getModel(); if (isReadOnly(activeModel, this.textFileService)) { this.notify(localize('applyCodeBlock.readonly', "Cannot apply code block to read-only file.")); @@ -295,7 +321,7 @@ export class ApplyCodeBlockOperation { }, () => cancellationTokenSource.cancel() ); - editsProposed = await this.applyWithInlinePreview(iterable, codeEditor, cancellationTokenSource); + editsProposed = await this.applyWithInlinePreview(iterable, codeEditor, cancellationTokenSource, applyCodeBlockSuggestionId); } catch (e) { if (!isCancellationError(e)) { this.notify(localize('applyCodeBlock.error', "Failed to apply code block: {0}", e.message)); @@ -375,8 +401,8 @@ export class ApplyCodeBlockOperation { }; } - private async applyWithInlinePreview(edits: AsyncIterable, codeEditor: IActiveCodeEditor, tokenSource: CancellationTokenSource): Promise { - return this.instantiationService.invokeFunction(reviewEdits, codeEditor, edits, tokenSource.token); + private async applyWithInlinePreview(edits: AsyncIterable, codeEditor: IActiveCodeEditor, tokenSource: CancellationTokenSource, applyCodeBlockSuggestionId: EditSuggestionId | undefined): Promise { + return this.instantiationService.invokeFunction(reviewEdits, codeEditor, edits, tokenSource.token, applyCodeBlockSuggestionId); } private async applyNotebookEditsWithInlinePreview(edits: AsyncIterable<[URI, TextEdit[]] | ICellEditOperation[]>, uri: URI, tokenSource: CancellationTokenSource): Promise { diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index c564db26a32..f201660eb0f 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -14,6 +14,7 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; +import { EditDeltaInfo } from '../../../../editor/common/textModelEditSource.js'; import { IChatAgentCommand, IChatAgentData } from '../common/chatAgents.js'; import { IChatResponseModel } from '../common/chatModel.js'; import { IParsedChatRequest } from '../common/chatParserTypes.js'; @@ -106,6 +107,8 @@ export interface IChatCodeBlockInfo { readonly isStreaming: boolean; readonly chatSessionId: string; focus(): void; + readonly languageId?: string | undefined; + readonly editDeltaInfo?: EditDeltaInfo | undefined; } export interface IChatFileTreeInfo { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index d72800f7508..1193f9baa8b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -37,6 +37,8 @@ import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js'; +import { EditDeltaInfo } from '../../../../../editor/common/textModelEditSource.js'; import { MarkedKatexSupport } from '../../../markdown/browser/markedKatexSupport.js'; import { IMarkdownVulnerability } from '../../common/annotations.js'; import { IEditSessionEntryDiff } from '../../common/chatEditingService.js'; @@ -94,6 +96,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP @IConfigurationService configurationService: IConfigurationService, @ITextModelService private readonly textModelService: ITextModelService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @IAiEditTelemetryService private readonly aiEditTelemetryService: IAiEditTelemetryService, ) { super(); @@ -195,6 +198,8 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP readonly elementId = element.id; readonly isStreaming = false; readonly chatSessionId = element.sessionId; + readonly languageId = languageId; + readonly editDeltaInfo = EditDeltaInfo.fromText(text); codemapperUri = undefined; // will be set async public get uri() { // here we must do a getter because the ref.object is rendered @@ -236,6 +241,8 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP public focus() { return ref.object.element.focus(); } + readonly languageId = languageId; + readonly editDeltaInfo = EditDeltaInfo.fromText(text); }(); this.codeblocks.push(info); orderedDisposablesList.push(ref); @@ -248,6 +255,23 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP ...markdownRenderOptions, }, this.domNode)); + // Ideally this would happen earlier, but we need to parse the markdown. + if (isResponseVM(element) && !element.model.codeBlockInfos && element.model.isComplete) { + element.model.initializeCodeBlockInfos(this.codeblocks.map(info => { + return { + suggestionId: this.aiEditTelemetryService.createSuggestionId({ + presentation: 'codeBlock', + feature: 'sideBarChat', + editDeltaInfo: info.editDeltaInfo, + languageId: info.languageId, + modeId: element.model.request?.modeInfo?.modeId, + modelId: element.model.request?.modelId, + applyCodeBlockSuggestionId: undefined, + }) + }; + })); + } + const markdownDecorationsRenderer = instantiationService.createInstance(ChatMarkdownDecorationsRenderer); this._register(markdownDecorationsRenderer.walkTreeAndAnnotateReferenceLinks(markdown, result.element)); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts index 5cc4c7eeedf..7a031f8bf6a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts @@ -24,6 +24,7 @@ import { IUndoRedoElement, IUndoRedoService } from '../../../../../platform/undo import { IEditorPane, SaveReason } from '../../../../common/editor.js'; import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js'; import { ITextFileService, isTextFileEditorModel, stringToSnapshot } from '../../../../services/textfile/common/textfiles.js'; +import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js'; import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js'; import { ChatEditKind, IModifiedEntryTelemetryInfo, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, ISnapshotEntry, ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel } from '../../common/chatModel.js'; @@ -85,7 +86,8 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie @ITextFileService private readonly _textFileService: ITextFileService, @IFileService fileService: IFileService, @IUndoRedoService undoRedoService: IUndoRedoService, - @IInstantiationService instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + @IAiEditTelemetryService aiEditTelemetryService: IAiEditTelemetryService, ) { super( resourceRef.object.textEditorModel.uri, @@ -96,7 +98,8 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie chatService, fileService, undoRedoService, - instantiationService + instantiationService, + aiEditTelemetryService, ); this._docFileEditorModel = this._register(resourceRef).object; @@ -126,6 +129,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie kind: 'chatEditingHunkAction', uri: this.modifiedURI, outcome: action.state, + languageId: this.modifiedModel.getLanguageId(), ...action }); })); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index 119e0c29dca..b23b06910c7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -20,6 +20,8 @@ import { editorBackground, registerColor, transparent } from '../../../../../pla import { IUndoRedoElement, IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js'; import { IEditorPane } from '../../../../common/editor.js'; import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js'; +import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js'; +import { EditDeltaInfo } from '../../../../../editor/common/textModelEditSource.js'; import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js'; import { ChatEditKind, IModifiedEntryTelemetryInfo, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, ISnapshotEntry, ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel } from '../../common/chatModel.js'; @@ -102,6 +104,7 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im @IFileService protected readonly _fileService: IFileService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, @IInstantiationService protected readonly _instantiationService: IInstantiationService, + @IAiEditTelemetryService private readonly _aiEditTelemetryService: IAiEditTelemetryService, ) { super(); @@ -242,9 +245,30 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im } protected _notifyAction(action: ChatUserAction) { + if (action.kind === 'chatEditingHunkAction') { + this._aiEditTelemetryService.handleCodeAccepted({ + suggestionId: undefined, // TODO@hediet try to figure this out + acceptanceMethod: 'accept', + presentation: 'highlightedEdit', + modelId: this._telemetryInfo.modelId, + modeId: this._telemetryInfo.modeId, + applyCodeBlockSuggestionId: this._telemetryInfo.applyCodeBlockSuggestionId, + editDeltaInfo: new EditDeltaInfo( + action.linesAdded, + action.linesRemoved, + -1, + -1, + ), + feature: this._telemetryInfo.feature, + languageId: action.languageId, + }); + } + this._chatService.notifyUserAction({ action, agentId: this._telemetryInfo.agentId, + modelId: this._telemetryInfo.modelId, + modeId: this._telemetryInfo.modeId, command: this._telemetryInfo.command, sessionId: this._telemetryInfo.sessionId, requestId: this._telemetryInfo.requestId, diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts index 46c15d6f6a8..bc1f20958fd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts @@ -53,6 +53,7 @@ import { ChatEditingNotebookDiffEditorIntegration, ChatEditingNotebookEditorInte import { ChatEditingNotebookFileSystemProvider } from './notebook/chatEditingNotebookFileSystemProvider.js'; import { adjustCellDiffAndOriginalModelBasedOnCellAddDelete, adjustCellDiffAndOriginalModelBasedOnCellMovements, adjustCellDiffForKeepingAnInsertedCell, adjustCellDiffForRevertingADeletedCell, adjustCellDiffForRevertingAnInsertedCell, calculateNotebookRewriteRatio, getCorrespondingOriginalCellIndex, isTransientIPyNbExtensionEvent } from './notebook/helpers.js'; import { countChanges, ICellDiffInfo, sortCellChanges } from './notebook/notebookCellChanges.js'; +import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js'; const SnapshotLanguageId = 'VSCodeChatNotebookSnapshotLanguage'; @@ -184,8 +185,9 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie @INotebookEditorWorkerService private readonly notebookEditorWorkerService: INotebookEditorWorkerService, @INotebookLoggingService private readonly loggingService: INotebookLoggingService, @INotebookEditorModelResolverService private readonly notebookResolver: INotebookEditorModelResolverService, + @IAiEditTelemetryService aiEditTelemetryService: IAiEditTelemetryService, ) { - super(modifiedResourceRef.object.notebook.uri, telemetryInfo, kind, configurationService, fileConfigService, chatService, fileService, undoRedoService, instantiationService); + super(modifiedResourceRef.object.notebook.uri, telemetryInfo, kind, configurationService, fileConfigService, chatService, fileService, undoRedoService, instantiationService, aiEditTelemetryService); this.initialContentComparer = new SnapshotComparer(initialContent); this.modifiedModel = this._register(modifiedResourceRef).object.notebook; this.originalModel = this._register(originalResourceRef).object.notebook; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 534dc146bf3..4527f5b1639 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -33,6 +33,7 @@ import { INotebookService } from '../../../notebook/common/notebookService.js'; import { ChatEditingSessionState, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IModifiedEntryTelemetryInfo, IModifiedFileEntry, ISnapshotEntry, IStreamingEdits, ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; +import { ChatAgentLocation } from '../../common/constants.js'; import { ChatEditingModifiedDocumentEntry } from './chatEditingModifiedDocumentEntry.js'; import { AbstractChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js'; import { ChatEditingModifiedNotebookEntry } from './chatEditingModifiedNotebookEntry.js'; @@ -509,12 +510,24 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio private _getTelemetryInfoForModel(responseModel: IChatResponseModel): IModifiedEntryTelemetryInfo { // Make these getters because the response result is not available when the file first starts to be edited - return new class { + return new class implements IModifiedEntryTelemetryInfo { get agentId() { return responseModel.agent?.id; } + get modelId() { return responseModel.request?.modelId; } + get modeId() { return responseModel.request?.modeInfo?.modeId; } get command() { return responseModel.slashCommand?.name; } get sessionId() { return responseModel.session.sessionId; } get requestId() { return responseModel.requestId; } get result() { return responseModel.result; } + get applyCodeBlockSuggestionId() { return responseModel.request?.modeInfo?.applyCodeBlockSuggestionId; } + + get feature(): string { + if (responseModel.session.initialLocation === ChatAgentLocation.Panel) { + return 'sideBarChat'; + } else if (responseModel.session.initialLocation === ChatAgentLocation.Editor) { + return 'inlineChat'; + } + return responseModel.session.initialLocation; + } }; } @@ -557,6 +570,10 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio const existingExternalEntry = this._lookupExternalEntry(resource); if (existingExternalEntry) { entry = existingExternalEntry; + + if (telemetryInfo.requestId !== entry.telemetryInfo.requestId) { + entry.updateTelemetryInfo(telemetryInfo); + } } else { const initialContent = this._initialFileContents.get(resource); // This gets manually disposed in .dispose() or in .restoreSnapshot() diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts index 4a01ea62583..0fe47f5ca10 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts @@ -12,6 +12,7 @@ import { IEnvironmentService } from '../../../../../platform/environment/common/ import { IFileService } from '../../../../../platform/files/common/files.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; +import { EditSuggestionId } from '../../../../../editor/common/textModelEditSource.js'; import { WorkingSetDisplayMetadata, ModifiedFileEntryState, ISnapshotEntry } from '../../common/chatEditingService.js'; const STORAGE_CONTENTS_FOLDER = 'contents'; @@ -80,7 +81,17 @@ export class ChatEditingSessionStorage { current: await getFileContent(entry.currentHash), state: entry.state, snapshotUri: URI.parse(entry.snapshotUri), - telemetryInfo: { requestId: entry.telemetryInfo.requestId, agentId: entry.telemetryInfo.agentId, command: entry.telemetryInfo.command, sessionId: this.chatSessionId, result: undefined } + telemetryInfo: { + requestId: entry.telemetryInfo.requestId, + agentId: entry.telemetryInfo.agentId, + command: entry.telemetryInfo.command, + sessionId: this.chatSessionId, + result: undefined, + modelId: entry.telemetryInfo.modelId, + modeId: entry.telemetryInfo.modeId, + applyCodeBlockSuggestionId: entry.telemetryInfo.applyCodeBlockSuggestionId, + feature: entry.telemetryInfo.feature, + } } satisfies ISnapshotEntry; }; try { @@ -190,7 +201,7 @@ export class ChatEditingSessionStorage { currentHash: await addFileContent(entry.current), state: entry.state, snapshotUri: entry.snapshotUri.toString(), - telemetryInfo: { requestId: entry.telemetryInfo.requestId, agentId: entry.telemetryInfo.agentId, command: entry.telemetryInfo.command } + telemetryInfo: { requestId: entry.telemetryInfo.requestId, agentId: entry.telemetryInfo.agentId, command: entry.telemetryInfo.command, modelId: entry.telemetryInfo.modelId, modeId: entry.telemetryInfo.modeId } }; }; @@ -281,6 +292,11 @@ interface IModifiedEntryTelemetryInfoDTO { readonly requestId: string; readonly agentId?: string; readonly command?: string; + + readonly modelId?: string; + readonly modeId?: 'ask' | 'edit' | 'agent' | 'custom' | 'applyCodeBlock' | undefined; + readonly applyCodeBlockSuggestionId?: EditSuggestionId | undefined; + readonly feature?: string; } type ResourceMapDTO = [string, T][]; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTextModelChangeService.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTextModelChangeService.ts index a7bdc1955aa..8fcd12e5419 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTextModelChangeService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTextModelChangeService.ts @@ -172,11 +172,6 @@ export class ChatEditingTextModelChangeService extends Disposable { const sessionId = responseModel.session.sessionId; const request = responseModel.session.getRequests().at(-1); const languageId = this.modifiedModel.getLanguageId(); - const mode: 'ask' | 'agent' | 'edit' | 'custom' | undefined = ( - (request?.modeInfo && !request.modeInfo.isBuiltin) - ? 'custom' - : request?.modeInfo?.kind - ); const agent = responseModel.agent; const extensionId = VersionedExtensionId.tryCreate(agent?.extensionId.value, agent?.extensionVersion); @@ -185,8 +180,9 @@ export class ChatEditingTextModelChangeService extends Disposable { requestId: request?.id, sessionId: sessionId, languageId, - mode, + mode: request?.modeInfo?.modeId, extensionId, + codeBlockSuggestionId: request?.modeInfo?.applyCodeBlockSuggestionId, }); if (isAtomicEdits) { diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 544027868b4..37b4ce5606e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -312,10 +312,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge public get currentModeInfo(): IChatRequestModeInfo { const mode = this._currentModeObservable.get(); + const modeId: 'ask' | 'agent' | 'edit' | 'custom' | undefined = mode.isBuiltin ? this.currentModeKind : 'custom'; + return { kind: this.currentModeKind, isBuiltin: mode.isBuiltin, - instructions: mode.body?.get() + instructions: mode.body?.get(), + modeId: modeId, + applyCodeBlockSuggestionId: undefined, }; } diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index e51607399ae..707f8f6a8c0 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -14,6 +14,7 @@ import { localize } from '../../../../nls.js'; import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IEditorPane } from '../../../common/editor.js'; +import { EditSuggestionId } from '../../../../editor/common/textModelEditSource.js'; import { ICellEditOperation } from '../../notebook/common/notebookCommon.js'; import { IChatAgentResult } from './chatAgents.js'; import { ChatModel, IChatResponseModel } from './chatModel.js'; @@ -85,6 +86,10 @@ export interface IModifiedEntryTelemetryInfo { readonly sessionId: string; readonly requestId: string; readonly result: IChatAgentResult | undefined; + readonly modelId: string | undefined; + readonly modeId: 'ask' | 'edit' | 'agent' | 'custom' | 'applyCodeBlock' | undefined; + readonly applyCodeBlockSuggestionId: EditSuggestionId | undefined; + readonly feature: 'sideBarChat' | 'inlineChat' | string | undefined; } export interface ISnapshotEntry { diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index b049332d316..af5fbeb0eb0 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -29,6 +29,8 @@ import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatResponseClearToPreviousToolInvocationReason, IChatAgentMarkdownContentWithVulnerability, IChatClearToPreviousToolInvocation, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatEditingSessionAction, IChatElicitationRequest, IChatExtensionsContent, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatMultiDiffData, IChatNotebookEdit, IChatPrepareToolInvocationPart, IChatProgress, IChatProgressMessage, IChatPullRequestContent, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatTask, IChatTaskSerialized, IChatTextEdit, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; import { IChatRequestVariableEntry } from './chatVariableEntries.js'; import { ChatAgentLocation, ChatModeKind } from './constants.js'; +import { EditSuggestionId } from '../../../../editor/common/textModelEditSource.js'; +import { BugIndicatingError } from '../../../../base/common/errors.js'; export const CHAT_ATTACHABLE_IMAGE_MIME_TYPES: Record = { @@ -68,6 +70,10 @@ export interface IChatRequestModel { readonly modelId?: string; } +export interface ICodeBlockInfo { + readonly suggestionId: EditSuggestionId; +} + export interface IChatTextEditGroupState { sha1: string; applied: number; @@ -155,6 +161,7 @@ export interface IChatResponseModel { readonly onDidChange: Event; readonly id: string; readonly requestId: string; + readonly request: IChatRequestModel | undefined; readonly username: string; readonly avatarIcon?: ThemeIcon | URI; readonly session: IChatModel; @@ -182,6 +189,9 @@ export interface IChatResponseModel { readonly voteDownReason: ChatAgentVoteDownReason | undefined; readonly followups?: IChatFollowup[] | undefined; readonly result?: IChatAgentResult; + readonly codeBlockInfos: ICodeBlockInfo[] | undefined; + + initializeCodeBlockInfos(codeBlockInfo: ICodeBlockInfo[]): void; addUndoStop(undoStop: IChatUndoStop): void; setVote(vote: ChatAgentVoteDirection): void; setVoteDownReason(reason: ChatAgentVoteDownReason | undefined): void; @@ -202,9 +212,11 @@ export type ChatResponseModelChangeReason = const defaultChatResponseModelChangeReason: ChatResponseModelChangeReason = { reason: 'other' }; export interface IChatRequestModeInfo { - kind: ChatModeKind; + kind: ChatModeKind | undefined; // is undefined in case of modeId == 'apply' isBuiltin: boolean; instructions: string | undefined; + modeId: 'ask' | 'agent' | 'edit' | 'custom' | 'applyCodeBlock' | undefined; + applyCodeBlockSuggestionId: EditSuggestionId | undefined; } export interface IChatRequestModelParameters { @@ -696,6 +708,10 @@ export interface IChatResponseModelParameters { shouldBeRemovedOnSend?: IChatRequestDisablement; shouldBeBlocked?: boolean; restoredId?: string; + /** + * undefined means it will be set later. + */ + codeBlockInfos: ICodeBlockInfo[] | undefined; } export class ChatResponseModel extends Disposable implements IChatResponseModel { @@ -720,6 +736,10 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel return this._shouldBeBlocked; } + public get request(): IChatRequestModel | undefined { + return this.session.getRequests().find(r => r.id === this.requestId); + } + public get session() { return this._session; } @@ -830,6 +850,10 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel return this._responseView; } + private _codeBlockInfos: ICodeBlockInfo[] | undefined; + public get codeBlockInfos(): ICodeBlockInfo[] | undefined { + return this._codeBlockInfos; + } constructor(params: IChatResponseModelParameters) { super(); @@ -852,6 +876,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel this._isStale = Array.isArray(params.responseContent) && (params.responseContent.length !== 0 || isMarkdownString(params.responseContent) && params.responseContent.value.length !== 0); this._response = this._register(new Response(params.responseContent)); + this._codeBlockInfos = params.codeBlockInfos ? [...params.codeBlockInfos] : undefined; const signal = observableSignalFromEvent(this, this.onDidChange); @@ -889,6 +914,13 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel })); } + initializeCodeBlockInfos(codeBlockInfo: ICodeBlockInfo[]): void { + if (this._codeBlockInfos) { + throw new BugIndicatingError('Code block infos have already been initialized'); + } + this._codeBlockInfos = [...codeBlockInfo]; + } + /** * Apply a progress update to the actual response content. */ @@ -1059,6 +1091,12 @@ export interface ISerializableChatRequestData { confirmation?: string; editedFileEvents?: IChatAgentEditedFileEvent[]; modelId?: string; + + responseMarkdownInfo: ISerializableMarkdownInfo[] | undefined; +} + +export interface ISerializableMarkdownInfo { + readonly suggestionId: EditSuggestionId; } export interface IExportableChatData { @@ -1470,6 +1508,7 @@ export class ChatModel extends Disposable implements IChatModel { followups: raw.followups, restoredId: raw.responseId, shouldBeBlocked: request.shouldBeBlocked, + codeBlockInfos: raw.responseMarkdownInfo?.map(info => ({ suggestionId: info.suggestionId })), }); request.response.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend; if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway? @@ -1618,7 +1657,8 @@ export class ChatModel extends Disposable implements IChatModel { agent: chatAgent, slashCommand, requestId: request.id, - isCompleteAddedRequest + isCompleteAddedRequest, + codeBlockInfos: undefined, }); this._requests.push(request); @@ -1661,7 +1701,8 @@ export class ChatModel extends Disposable implements IChatModel { request.response = new ChatResponseModel({ responseContent: [], session: this, - requestId: request.id + requestId: request.id, + codeBlockInfos: undefined, }); } @@ -1709,7 +1750,8 @@ export class ChatModel extends Disposable implements IChatModel { request.response = new ChatResponseModel({ responseContent: [], session: this, - requestId: request.id + requestId: request.id, + codeBlockInfos: undefined, }); } @@ -1780,6 +1822,7 @@ export class ChatModel extends Disposable implements IChatModel { responseId: r.response?.id, shouldBeRemovedOnSend: r.shouldBeRemovedOnSend, result: r.response?.result, + responseMarkdownInfo: r.response?.codeBlockInfos?.map(info => ({ suggestionId: info.suggestionId })), followups: r.response?.followups, isCanceled: r.response?.isCanceled, vote: r.response?.vote, diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 8ea19ae391f..446b23fac82 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -553,6 +553,9 @@ export interface IChatEditingHunkAction { linesRemoved: number; outcome: 'accepted' | 'rejected'; hasRemainingEdits: boolean; + modeId?: string; + modelId?: string; + languageId?: string; } export type ChatUserAction = IChatVoteAction | IChatCopyAction | IChatInsertAction | IChatApplyAction | IChatTerminalAction | IChatCommandAction | IChatFollowupAction | IChatBugReportAction | IChatInlineChatCodeAction | IChatEditingSessionAction | IChatEditingHunkAction; @@ -564,6 +567,8 @@ export interface IChatUserActionEvent { sessionId: string; requestId: string; result: IChatAgentResult | undefined; + modelId?: string | undefined; + modeId?: string | undefined; } export interface IChatDynamicRequest { diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts index 50d651b7bce..70b3a24e3dc 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts @@ -48,7 +48,7 @@ suite('ChatEditingSessionStorage', () => { return { stopId, entries: new ResourceMap([ - [resource, { resource, languageId: 'javascript', snapshotUri: ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(sessionId, requestId, stopId, resource.path), original: `contents${before}}`, current: `contents${after}`, state: ModifiedFileEntryState.Modified, telemetryInfo: { agentId: 'agentId', command: 'cmd', requestId: generateUuid(), result: undefined, sessionId } } satisfies ISnapshotEntry], + [resource, { resource, languageId: 'javascript', snapshotUri: ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(sessionId, requestId, stopId, resource.path), original: `contents${before}}`, current: `contents${after}`, state: ModifiedFileEntryState.Modified, telemetryInfo: { agentId: 'agentId', command: 'cmd', requestId: generateUuid(), result: undefined, sessionId, modelId: undefined, modeId: undefined, applyCodeBlockSuggestionId: undefined, feature: undefined } } satisfies ISnapshotEntry], ]), }; } diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap index e1128cdbe6a..84d93d91194 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap @@ -60,6 +60,7 @@ responseId: undefined, shouldBeRemovedOnSend: undefined, result: { metadata: { metadataKey: "value" } }, + responseMarkdownInfo: undefined, followups: undefined, isCanceled: false, vote: undefined, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap index c98aa1128f3..dcb2adcc070 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap @@ -60,6 +60,7 @@ responseId: undefined, shouldBeRemovedOnSend: undefined, result: { errorDetails: { message: "No activated agent with id \"ChatProviderWithUsedContext\"" } }, + responseMarkdownInfo: undefined, followups: undefined, isCanceled: false, vote: undefined, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap index 7c5955503e9..22c4eb64b28 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap @@ -61,6 +61,7 @@ responseId: undefined, shouldBeRemovedOnSend: undefined, result: { metadata: { metadataKey: "value" } }, + responseMarkdownInfo: undefined, followups: [ { kind: "reply", @@ -140,6 +141,7 @@ responseId: undefined, shouldBeRemovedOnSend: undefined, result: { }, + responseMarkdownInfo: undefined, followups: [ ], isCanceled: false, vote: undefined, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap index 25f0e6ee986..c3a5982cd8f 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap @@ -61,6 +61,7 @@ responseId: undefined, shouldBeRemovedOnSend: undefined, result: { errorDetails: { message: "No activated agent with id \"ChatProviderWithUsedContext\"" } }, + responseMarkdownInfo: undefined, followups: undefined, isCanceled: false, vote: undefined, diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts index a1f24936272..16fd396aabd 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts @@ -10,6 +10,9 @@ import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '. import { localize } from '../../../../nls.js'; import { EDIT_TELEMETRY_DETAILS_SETTING_ID, EDIT_TELEMETRY_SHOW_DECORATIONS, EDIT_TELEMETRY_SHOW_STATUS_BAR } from './settings.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { IAiEditTelemetryService } from './telemetry/aiEditTelemetry/aiEditTelemetryService.js'; +import { AiEditTelemetryServiceImpl } from './telemetry/aiEditTelemetry/aiEditTelemetryServiceImpl.js'; registerWorkbenchContribution2('EditTelemetryContribution', EditTelemetryContribution, WorkbenchPhase.AfterRestored); @@ -58,3 +61,5 @@ configurationRegistry.registerConfiguration({ }, } }); + +registerSingleton(IAiEditTelemetryService, AiEditTelemetryServiceImpl, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.ts b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.ts new file mode 100644 index 00000000000..563c97f6003 --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EditDeltaInfo, EditSuggestionId } from '../../../../../../editor/common/textModelEditSource.js'; +import { createDecorator } from '../../../../../../platform/instantiation/common/instantiation.js'; + +export const IAiEditTelemetryService = createDecorator('aiEditTelemetryService'); + +export interface IAiEditTelemetryService { + readonly _serviceBrand: undefined; + + createSuggestionId(data: Omit): EditSuggestionId; + + handleCodeAccepted(data: IEditTelemetryCodeAcceptedData): void; +} + +export interface IEditTelemetryBaseData { + suggestionId: EditSuggestionId | undefined; + + presentation: 'codeBlock' | 'highlightedEdit' | 'inlineSuggestion'; + feature: 'sideBarChat' | 'inlineChat' | 'inlineSuggestion' | string | undefined; + + languageId: string | undefined; + + editDeltaInfo: EditDeltaInfo | undefined; + + modeId: 'ask' | 'edit' | 'agent' | 'custom' | 'applyCodeBlock' | undefined; + applyCodeBlockSuggestionId: EditSuggestionId | undefined; // Is set if modeId is applyCodeBlock + + modelId: string | undefined; // e.g. 'gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo' +} + +export interface IEditTelemetryCodeSuggestedData extends IEditTelemetryBaseData { +} + +export interface IEditTelemetryCodeAcceptedData extends IEditTelemetryBaseData { + acceptanceMethod: + | 'insertAtCursor' + | 'insertInNewFile' + | 'copyManual' + | 'copyButton' + | 'accept'; // clicking on 'keep' or tab for inline completions +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryServiceImpl.ts b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryServiceImpl.ts new file mode 100644 index 00000000000..b1789533275 --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryServiceImpl.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 { generateUuid } from '../../../../../../base/common/uuid.js'; +import { EditSuggestionId } from '../../../../../../editor/common/textModelEditSource.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { DataChannelForwardingTelemetryService } from '../forwardingTelemetryService.js'; +import { IAiEditTelemetryService, IEditTelemetryCodeAcceptedData, IEditTelemetryCodeSuggestedData } from './aiEditTelemetryService.js'; + +export class AiEditTelemetryServiceImpl implements IAiEditTelemetryService { + declare readonly _serviceBrand: undefined; + + private readonly _telemetryService: ITelemetryService; + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + this._telemetryService = this.instantiationService.createInstance(DataChannelForwardingTelemetryService); + } + + public createSuggestionId(data: Omit): EditSuggestionId { + const suggestionId = EditSuggestionId.newId(); + this._telemetryService.publicLog2<{ + eventId: string | undefined; + suggestionId: string | undefined; + + presentation: 'codeBlock' | 'highlightedEdit' | 'inlineSuggestion' | undefined; + feature: 'sideBarChat' | 'inlineChat' | 'inlineSuggestion' | string | undefined; + + editCharsInserted: number | undefined; + editCharsDeleted: number | undefined; + editLinesInserted: number | undefined; + editLinesDeleted: number | undefined; + + modeId: 'ask' | 'edit' | 'agent' | 'custom' | 'applyCodeBlock' | undefined; + modelId: string | undefined; + + applyCodeBlockSuggestionId: string | undefined; + languageId: string | undefined; + + }, { + owner: 'hediet'; + comment: 'Reports when code is suggested to the user for editing.'; + + eventId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Unique identifier for this event.' }; + suggestionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Unique identifier for this suggestion.' }; + + presentation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How the code suggestion is presented to the user.' }; + feature: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Where in the UI the code suggestion is shown.' }; + + editCharsInserted: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of characters inserted in the edit.' }; + editCharsDeleted: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of characters deleted in the edit.' }; + editLinesInserted: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of lines inserted in the edit.' }; + editLinesDeleted: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of lines deleted in the edit.' }; + + modeId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The mode or type of editing session.' }; + modelId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The AI model used to generate the suggestion.' }; + + applyCodeBlockSuggestionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If the suggestion is for applying a code block, this is the ID of that suggestion.' }; + languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The programming language of the code suggestion.' }; + + }>('editTelemetry.codeSuggested', { + eventId: generateUuid(), + suggestionId: suggestionId as unknown as string, + presentation: data.presentation, + feature: data.feature, + editCharsInserted: data.editDeltaInfo?.charsAdded, + editCharsDeleted: data.editDeltaInfo?.charsRemoved, + editLinesInserted: data.editDeltaInfo?.linesAdded, + editLinesDeleted: data.editDeltaInfo?.linesRemoved, + modeId: data.modeId, + modelId: data.modelId, + applyCodeBlockSuggestionId: data.applyCodeBlockSuggestionId as unknown as string, + languageId: data.languageId, + }); + + return suggestionId; + } + + public handleCodeAccepted(data: IEditTelemetryCodeAcceptedData): void { + this._telemetryService.publicLog2<{ + eventId: string | undefined; + suggestionId: string | undefined; + + presentation: 'codeBlock' | 'highlightedEdit' | 'inlineSuggestion' | undefined; + feature: 'sideBarChat' | 'inlineChat' | 'inlineSuggestion' | string | undefined; + + editCharsInserted: number | undefined; + editCharsDeleted: number | undefined; + editLinesInserted: number | undefined; + editLinesDeleted: number | undefined; + + modeId: 'ask' | 'edit' | 'agent' | 'custom' | 'applyCodeBlock' | undefined; + modelId: string | undefined; + + applyCodeBlockSuggestionId: string | undefined; + languageId: string | undefined; + acceptanceMethod: + | 'insertAtCursor' + | 'insertInNewFile' + | 'copyManual' + | 'copyButton' + | 'applyCodeBlock' + | 'accept'; + }, { + owner: 'hediet'; + comment: 'Reports when code is suggested to the user for editing.'; + + eventId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Unique identifier for this event.' }; + suggestionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Unique identifier for this suggestion.' }; + + presentation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How the code suggestion is presented to the user.' }; + feature: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Where in the UI the code suggestion is shown.' }; + + editCharsInserted: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of characters inserted in the edit.' }; + editCharsDeleted: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of characters deleted in the edit.' }; + editLinesInserted: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of lines inserted in the edit.' }; + editLinesDeleted: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of lines deleted in the edit.' }; + + modeId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The mode or type of editing session.' }; + modelId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The AI model used to generate the suggestion.' }; + + applyCodeBlockSuggestionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If the suggestion is for applying a code block, this is the ID of that suggestion.' }; + languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The programming language of the code suggestion.' }; + acceptanceMethod: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How the user accepted the code suggestion.' }; + + }>('editTelemetry.codeAccepted', { + eventId: generateUuid(), + suggestionId: data.suggestionId as unknown as string, + presentation: data.presentation, + feature: data.feature, + editCharsInserted: data.editDeltaInfo?.charsAdded, + editCharsDeleted: data.editDeltaInfo?.charsRemoved, + editLinesInserted: data.editDeltaInfo?.linesAdded, + editLinesDeleted: data.editDeltaInfo?.linesRemoved, + modeId: data.modeId, + modelId: data.modelId, + applyCodeBlockSuggestionId: data.applyCodeBlockSuggestionId as unknown as string, + languageId: data.languageId, + acceptanceMethod: data.acceptanceMethod, + }); + } +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.ts b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.ts index dc25bf50bcf..8b068e5694e 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.ts @@ -6,18 +6,19 @@ import { sumBy } from '../../../../../base/common/arrays.js'; import { TimeoutTimer } from '../../../../../base/common/async.js'; import { onUnexpectedError } from '../../../../../base/common/errors.js'; -import { Disposable, toDisposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { runOnChange, IObservableWithChange } from '../../../../../base/common/observable.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { IObservableWithChange, runOnChange } from '../../../../../base/common/observable.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; import { LineEdit } from '../../../../../editor/common/core/edits/lineEdit.js'; import { AnnotatedStringEdit, BaseStringEdit } from '../../../../../editor/common/core/edits/stringEdit.js'; import { StringText } from '../../../../../editor/common/core/text/abstractText.js'; +import { EditDeltaInfo, EditSuggestionId, ITextModelEditSourceMetadata } from '../../../../../editor/common/textModelEditSource.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; +import { EditSourceData, IDocumentWithAnnotatedEdits, createDocWithJustReason } from '../helpers/documentWithAnnotatedEdits.js'; +import { IAiEditTelemetryService } from './aiEditTelemetry/aiEditTelemetryService.js'; import { ArcTracker } from './arcTracker.js'; -import { IDocumentWithAnnotatedEdits, EditSourceData, createDocWithJustReason } from '../helpers/documentWithAnnotatedEdits.js'; import type { ScmRepoBridge } from './editSourceTrackingImpl.js'; -import { ITextModelEditSourceMetadata } from '../../../../../editor/common/textModelEditSource.js'; -import { generateUuid } from '../../../../../base/common/uuid.js'; import { forwardToChannelIf, isCopilotLikeExtension } from './forwardingTelemetryService.js'; export class InlineEditArcTelemetrySender extends Disposable { @@ -103,6 +104,51 @@ export class InlineEditArcTelemetrySender extends Disposable { } } +export class AiEditTelemetryAdapter extends Disposable { + constructor( + docWithAnnotatedEdits: IDocumentWithAnnotatedEdits, + @IAiEditTelemetryService private readonly _aiEditTelemetryService: IAiEditTelemetryService, + ) { + super(); + + this._register(runOnChange(docWithAnnotatedEdits.value, (_val, _prev, changes) => { + const edit = AnnotatedStringEdit.compose(changes.map(c => c.edit)); + + const supportedSource = new Set(['Chat.applyEdits', 'inlineChat.applyEdits'] as ITextModelEditSourceMetadata['source'][]); + + if (!edit.replacements.some(r => supportedSource.has(r.data.editSource.metadata.source))) { + return; + } + if (!edit.replacements.every(r => supportedSource.has(r.data.editSource.metadata.source))) { + onUnexpectedError(new Error(`ArcTelemetrySender: Not all edits are ${edit.replacements[0].data.editSource.metadata.source}!`)); + return; + } + let applyCodeBlockSuggestionId: EditSuggestionId | undefined = undefined; + const data = edit.replacements[0].data.editSource; + let feature: 'inlineChat' | 'sideBarChat'; + if (data.metadata.source === 'Chat.applyEdits') { + feature = 'sideBarChat'; + if (data.metadata.$$mode === 'applyCodeBlock') { + applyCodeBlockSuggestionId = data.metadata.$$codeBlockSuggestionId; + } + } else { + feature = 'inlineChat'; + } + + // TODO@hediet tie this suggestion id to hunks, so acceptance can be correlated. + this._aiEditTelemetryService.createSuggestionId({ + applyCodeBlockSuggestionId, + languageId: data.props.$$languageId, + presentation: 'highlightedEdit', + feature, + modelId: data.props.$modelId, + modeId: data.props.$$mode as any, + editDeltaInfo: EditDeltaInfo.fromEdit(edit, _prev), + }); + })); + } +} + export class ChatArcTelemetrySender extends Disposable { constructor( docWithAnnotatedEdits: IDocumentWithAnnotatedEdits, diff --git a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingImpl.ts b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingImpl.ts index 008fcc9dc45..5025e348259 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingImpl.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingImpl.ts @@ -14,7 +14,7 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { ISCMRepository, ISCMService } from '../../../scm/common/scm.js'; import { AnnotatedDocuments, AnnotatedDocument } from '../helpers/annotatedDocuments.js'; -import { ChatArcTelemetrySender, InlineEditArcTelemetrySender } from './arcTelemetrySender.js'; +import { AiEditTelemetryAdapter, ChatArcTelemetrySender, InlineEditArcTelemetrySender } from './arcTelemetrySender.js'; import { createDocWithJustReason, EditSource } from '../helpers/documentWithAnnotatedEdits.js'; import { DocumentEditSourceTracker, TrackedEdit } from './editTracker.js'; import { sumByCategory } from '../helpers/utils.js'; @@ -102,6 +102,7 @@ class TrackedDocumentInfo extends Disposable { this._store.add(this._instantiationService.createInstance(InlineEditArcTelemetrySender, _doc.documentWithAnnotations, repo)); this._store.add(this._instantiationService.createInstance(ChatArcTelemetrySender, _doc.documentWithAnnotations, repo)); + this._store.add(this._instantiationService.createInstance(AiEditTelemetryAdapter, _doc.documentWithAnnotations)); })(); const resetSignal = observableSignal('resetSignal'); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 24bf5c2dfb2..374beb58ae8 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -65,6 +65,7 @@ import { INotebookService } from '../../notebook/common/notebookService.js'; import { ICellEditOperation } from '../../notebook/common/notebookCommon.js'; import { INotebookEditor } from '../../notebook/browser/notebookBrowser.js'; import { isNotebookContainingCellEditor as isNotebookWithCellEditor } from '../../notebook/browser/notebookEditor.js'; +import { EditSuggestionId } from '../../../../editor/common/textModelEditSource.js'; export const enum State { CREATE_SESSION = 'CREATE_SESSION', @@ -1524,7 +1525,7 @@ export class InlineChatController2 implements IEditorContribution { } } -export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEditor, stream: AsyncIterable, token: CancellationToken): Promise { +export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEditor, stream: AsyncIterable, token: CancellationToken, applyCodeBlockSuggestionId: EditSuggestionId | undefined): Promise { if (!editor.hasModel()) { return false; } @@ -1541,7 +1542,13 @@ export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEdito store.add(chatModel); // STREAM - const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0); + const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0, { + kind: undefined, + modeId: 'applyCodeBlock', + instructions: undefined, + isBuiltin: true, + applyCodeBlockSuggestionId, + }); assertType(chatRequest.response); chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false }); for await (const chunk of stream) {