Implements editTelemetry.codeSuggested and editTelemetry.codeAccepted

This commit is contained in:
Henning Dieterichs
2025-08-27 02:06:27 +02:00
committed by Henning Dieterichs
parent e2c8943632
commit 9e1f3e6df0
31 changed files with 629 additions and 42 deletions

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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<IdentifiableInlineCompletions> = {
provideInlineCompletions: async (model: ITextModel, position: EditorPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise<IdentifiableInlineCompletions | undefined> => {
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<void> => {
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,

View File

@@ -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<IdentifiableInlineCompletion> {
pid: number;
languageId: string;
}
export interface IdentifiableInlineCompletion extends languages.InlineCompletion {
idx: number;
suggestionId: EditSuggestionId | undefined;
}
export interface MainThreadLanguageFeaturesShape extends IDisposable {

View File

@@ -1399,6 +1399,7 @@ class InlineCompletionAdapter {
return {
pid,
languageId: doc.languageId,
items: resultItems.map<extHostProtocol.IdentifiableInlineCompletion>((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 => {

View File

@@ -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;
}

View File

@@ -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<IComputeEditsResult | undefined> {
private async handleTextEditor(codeEditor: IActiveCodeEditor, chatSessionId: string | undefined, code: string, applyCodeBlockSuggestionId: EditSuggestionId | undefined): Promise<IComputeEditsResult | undefined> {
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<TextEdit[]>, codeEditor: IActiveCodeEditor, tokenSource: CancellationTokenSource): Promise<boolean> {
return this.instantiationService.invokeFunction(reviewEdits, codeEditor, edits, tokenSource.token);
private async applyWithInlinePreview(edits: AsyncIterable<TextEdit[]>, codeEditor: IActiveCodeEditor, tokenSource: CancellationTokenSource, applyCodeBlockSuggestionId: EditSuggestionId | undefined): Promise<boolean> {
return this.instantiationService.invokeFunction(reviewEdits, codeEditor, edits, tokenSource.token, applyCodeBlockSuggestionId);
}
private async applyNotebookEditsWithInlinePreview(edits: AsyncIterable<[URI, TextEdit[]] | ICellEditOperation[]>, uri: URI, tokenSource: CancellationTokenSource): Promise<boolean> {

View File

@@ -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 {

View File

@@ -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));

View File

@@ -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
});
}));

View File

@@ -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,

View File

@@ -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;

View File

@@ -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()

View File

@@ -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<T> = [string, T][];

View File

@@ -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) {

View File

@@ -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,
};
}

View File

@@ -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 {

View File

@@ -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<string, string> = {
@@ -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<ChatResponseModelChangeReason>;
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<ICodeBlockInfo>(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<ISerializableMarkdownInfo>(info => ({ suggestionId: info.suggestionId })),
followups: r.response?.followups,
isCanceled: r.response?.isCanceled,
vote: r.response?.vote,

View File

@@ -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 {

View File

@@ -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],
]),
};
}

View File

@@ -60,6 +60,7 @@
responseId: undefined,
shouldBeRemovedOnSend: undefined,
result: { metadata: { metadataKey: "value" } },
responseMarkdownInfo: undefined,
followups: undefined,
isCanceled: false,
vote: undefined,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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);

View File

@@ -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<IAiEditTelemetryService>('aiEditTelemetryService');
export interface IAiEditTelemetryService {
readonly _serviceBrand: undefined;
createSuggestionId(data: Omit<IEditTelemetryCodeSuggestedData, 'suggestionId'>): 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
}

View File

@@ -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<IEditTelemetryCodeSuggestedData, 'suggestionId'>): 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,
});
}
}

View File

@@ -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<EditSourceData>,
@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<EditSourceData>,

View File

@@ -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');

View File

@@ -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<TextEdit[]>, token: CancellationToken): Promise<boolean> {
export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEditor, stream: AsyncIterable<TextEdit[]>, token: CancellationToken, applyCodeBlockSuggestionId: EditSuggestionId | undefined): Promise<boolean> {
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) {