diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachment.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachment.ts index 04e66cf8dd0..6365407f923 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachment.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackAttachment.ts @@ -7,10 +7,8 @@ import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js' import { Codicon } from '../../../../base/common/codicons.js'; import { basename } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; -import { IRange } from '../../../../editor/common/core/range.js'; -import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { localize } from '../../../../nls.js'; -import { IAgentFeedbackService, IAgentFeedback } from './agentFeedbackService.js'; +import { IAgentFeedbackService } from './agentFeedbackService.js'; import { IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; import { IAgentFeedbackVariableEntry } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js'; @@ -28,13 +26,9 @@ export class AgentFeedbackAttachmentContribution extends Disposable { /** Track onDidAcceptInput subscriptions per widget session */ private readonly _widgetListeners = this._store.add(new DisposableMap()); - /** Cache of resolved code snippets keyed by feedback ID */ - private readonly _snippetCache = new Map(); - constructor( @IAgentFeedbackService private readonly _agentFeedbackService: IAgentFeedbackService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, - @ITextModelService private readonly _textModelService: ITextModelService, ) { super(); @@ -55,11 +49,10 @@ export class AgentFeedbackAttachmentContribution extends Disposable { if (feedbackItems.length === 0) { widget.attachmentModel.delete(attachmentId); - this._snippetCache.clear(); return; } - const value = await this._buildFeedbackValue(feedbackItems); + const value = this._buildFeedbackValue(feedbackItems); const entry: IAgentFeedbackVariableEntry = { kind: 'agentFeedback', @@ -74,7 +67,8 @@ export class AgentFeedbackAttachmentContribution extends Disposable { text: f.text, resourceUri: f.resourceUri, range: f.range, - codeSelection: this._snippetCache.get(f.id), + codeSelection: f.codeSelection, + diffHunks: f.diffHunks, })), value, }; @@ -85,41 +79,23 @@ export class AgentFeedbackAttachmentContribution extends Disposable { } /** - * Builds a rich string value for the agent feedback attachment that includes - * the code snippet at each feedback item's location alongside the feedback text. - * Uses a cache keyed by feedback ID to avoid re-resolving snippets for - * items that haven't changed. + * Builds a rich string value for the agent feedback attachment from + * the selection and diff context already stored on each feedback item. */ - private async _buildFeedbackValue(feedbackItems: readonly IAgentFeedback[]): Promise { - // Prune stale cache entries for items that no longer exist - const currentIds = new Set(feedbackItems.map(f => f.id)); - for (const cachedId of this._snippetCache.keys()) { - if (!currentIds.has(cachedId)) { - this._snippetCache.delete(cachedId); - } - } - - // Resolve only new (uncached) snippets - const uncachedItems = feedbackItems.filter(f => !this._snippetCache.has(f.id)); - if (uncachedItems.length > 0) { - await Promise.all(uncachedItems.map(async f => { - const snippet = await this._getCodeSnippet(f.resourceUri, f.range); - this._snippetCache.set(f.id, snippet); - })); - } - - // Build the final string from cache + private _buildFeedbackValue(feedbackItems: IAgentFeedbackVariableEntry['feedbackItems']): string { const parts: string[] = ['The following comments were made on the code changes:']; for (const item of feedbackItems) { - const codeSnippet = this._snippetCache.get(item.id); const fileName = basename(item.resourceUri); const lineRef = item.range.startLineNumber === item.range.endLineNumber ? `${item.range.startLineNumber}` : `${item.range.startLineNumber}-${item.range.endLineNumber}`; let part = `[${fileName}:${lineRef}]`; - if (codeSnippet) { - part += `\n\`\`\`\n${codeSnippet}\n\`\`\``; + if (item.codeSelection) { + part += `\nSelection:\n\`\`\`\n${item.codeSelection}\n\`\`\``; + } + if (item.diffHunks) { + part += `\nDiff Hunks:\n\`\`\`diff\n${item.diffHunks}\n\`\`\``; } part += `\nComment: ${item.text}`; parts.push(part); @@ -128,23 +104,6 @@ export class AgentFeedbackAttachmentContribution extends Disposable { return parts.join('\n\n'); } - /** - * Resolves the text model for a resource and extracts the code in the given range. - * Returns undefined if the model cannot be resolved. - */ - private async _getCodeSnippet(resourceUri: URI, range: IRange): Promise { - try { - const ref = await this._textModelService.createModelReference(resourceUri); - try { - return ref.object.textEditorModel.getValueInRange(range); - } finally { - ref.dispose(); - } - } catch { - return undefined; - } - } - /** * Ensure we listen for the chat widget's submit event so we can clear feedback after send. */ diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts index 1f881cc1223..1002f0c95a7 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorInputContribution.ts @@ -5,18 +5,19 @@ import './media/agentFeedbackEditorInput.css'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; +import { ICodeEditor, IDiffEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; -import { SelectionDirection } from '../../../../editor/common/core/selection.js'; +import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; import { URI } from '../../../../base/common/uri.js'; import { addStandardDisposableListener, getWindow, ModifierKeyEmitter } from '../../../../base/browser/dom.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { IAgentFeedbackService } from './agentFeedbackService.js'; import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; -import { getSessionForResource } from './agentFeedbackEditorUtils.js'; +import { createAgentFeedbackContext, getSessionForResource } from './agentFeedbackEditorUtils.js'; import { localize } from '../../../../nls.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { Action } from '../../../../base/common/actions.js'; @@ -205,6 +206,7 @@ export class AgentFeedbackEditorInputContribution extends Disposable implements @IAgentFeedbackService private readonly _agentFeedbackService: IAgentFeedbackService, @IChatEditingService private readonly _chatEditingService: IChatEditingService, @IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, ) { super(); @@ -278,7 +280,7 @@ export class AgentFeedbackEditorInputContribution extends Disposable implements } const selection = this._editor.getSelection(); - if (!selection || selection.isEmpty()) { + if (!selection || (selection.isEmpty() && !this._getDiffHunkForSelection(selection))) { this._hide(); return; } @@ -369,6 +371,16 @@ export class AgentFeedbackEditorInputContribution extends Disposable implements return; } + // Keep caret/navigation keys in the editor. Only actual typing should move focus. + if ( + e.keyCode === KeyCode.UpArrow + || e.keyCode === KeyCode.DownArrow + || e.keyCode === KeyCode.LeftArrow + || e.keyCode === KeyCode.RightArrow + ) { + return; + } + // Only auto-focus the input on typing when the document is readonly; // when editable the user must click or use Ctrl+I to focus. if (!this._editor.getOption(EditorOption.readOnly)) { @@ -462,7 +474,7 @@ export class AgentFeedbackEditorInputContribution extends Disposable implements return false; } - this._agentFeedbackService.addFeedback(this._sessionResource, model.uri, selection, text); + this._agentFeedbackService.addFeedback(this._sessionResource, model.uri, selection, text, undefined, createAgentFeedbackContext(this._editor, this._codeEditorService, model.uri, selection)); this._hideAndRefocusEditor(); return true; } @@ -485,7 +497,43 @@ export class AgentFeedbackEditorInputContribution extends Disposable implements const sessionResource = this._sessionResource; this._hideAndRefocusEditor(); - this._agentFeedbackService.addFeedbackAndSubmit(sessionResource, model.uri, selection, text); + this._agentFeedbackService.addFeedbackAndSubmit(sessionResource, model.uri, selection, text, undefined, createAgentFeedbackContext(this._editor, this._codeEditorService, model.uri, selection)); + } + + private _getContainingDiffEditor(): IDiffEditor | undefined { + return this._codeEditorService.listDiffEditors().find(diffEditor => + diffEditor.getModifiedEditor() === this._editor || diffEditor.getOriginalEditor() === this._editor + ); + } + + private _getDiffHunkForSelection(selection: Selection): { startLineNumber: number; endLineNumberExclusive: number } | undefined { + if (!selection.isEmpty()) { + return undefined; + } + + const diffEditor = this._getContainingDiffEditor(); + if (!diffEditor) { + return undefined; + } + + const diffResult = diffEditor.getDiffComputationResult(); + if (!diffResult) { + return undefined; + } + + const lineNumber = selection.getStartPosition().lineNumber; + const isModifiedEditor = diffEditor.getModifiedEditor() === this._editor; + for (const change of diffResult.changes2) { + const lineRange = isModifiedEditor ? change.modified : change.original; + if (!lineRange.isEmpty && lineRange.contains(lineNumber)) { + return { + startLineNumber: lineRange.startLineNumber, + endLineNumberExclusive: lineRange.endLineNumberExclusive, + }; + } + } + + return undefined; } private _updatePosition(): void { @@ -494,11 +542,50 @@ export class AgentFeedbackEditorInputContribution extends Disposable implements } const selection = this._editor.getSelection(); - if (!selection || selection.isEmpty()) { + if (!selection) { this._hide(); return; } + const lineHeight = this._editor.getOption(EditorOption.lineHeight); + const layoutInfo = this._editor.getLayoutInfo(); + const widgetDom = this._widget.getDomNode(); + const widgetHeight = widgetDom.offsetHeight || 30; + const widgetWidth = widgetDom.offsetWidth || 150; + + if (selection.isEmpty()) { + const diffHunk = this._getDiffHunkForSelection(selection); + if (!diffHunk) { + this._hide(); + return; + } + + const cursorPosition = selection.getStartPosition(); + const scrolledPosition = this._editor.getScrolledVisiblePosition(cursorPosition); + if (!scrolledPosition) { + this._widget.setPosition(null); + return; + } + + const hunkLineCount = diffHunk.endLineNumberExclusive - diffHunk.startLineNumber; + const cursorLineOffset = cursorPosition.lineNumber - diffHunk.startLineNumber; + const topHalfLineCount = Math.ceil(hunkLineCount / 2); + const top = hunkLineCount < 10 + ? cursorLineOffset < topHalfLineCount + ? scrolledPosition.top - (cursorLineOffset * lineHeight) - widgetHeight + : scrolledPosition.top + ((diffHunk.endLineNumberExclusive - cursorPosition.lineNumber) * lineHeight) + : scrolledPosition.top - widgetHeight; + const left = Math.max(0, Math.min(scrolledPosition.left, layoutInfo.width - widgetWidth)); + + this._widget.setPosition({ + preference: { + top: Math.max(0, Math.min(top, layoutInfo.height - widgetHeight)), + left, + } + }); + return; + } + const cursorPosition = selection.getDirection() === SelectionDirection.LTR ? selection.getEndPosition() : selection.getStartPosition(); @@ -509,12 +596,6 @@ export class AgentFeedbackEditorInputContribution extends Disposable implements return; } - const lineHeight = this._editor.getOption(EditorOption.lineHeight); - const layoutInfo = this._editor.getLayoutInfo(); - const widgetDom = this._widget.getDomNode(); - const widgetHeight = widgetDom.offsetHeight || 30; - const widgetWidth = widgetDom.offsetWidth || 150; - // Compute vertical position, flipping if out of bounds let top: number; if (selection.getDirection() === SelectionDirection.LTR) { diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorUtils.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorUtils.ts index bb42a4ff242..3a9da1e5230 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorUtils.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorUtils.ts @@ -4,10 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from '../../../../base/common/uri.js'; +import { isEqual } from '../../../../base/common/resources.js'; +import { ICodeEditor, IDiffEditor } from '../../../../editor/browser/editorBrowser.js'; +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { IRange } from '../../../../editor/common/core/range.js'; +import { DetailedLineRangeMapping } from '../../../../editor/common/diff/rangeMapping.js'; import { EditorResourceAccessor, SideBySideEditor } from '../../../../workbench/common/editor.js'; import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { agentSessionContainsResource, editingEntriesContainResource } from '../../../../workbench/contrib/chat/browser/sessionResourceMatching.js'; +import { IChatSessionFileChange, IChatSessionFileChange2, isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; /** * Find the session that contains the given resource by checking editing sessions and agent sessions. @@ -32,6 +38,279 @@ export function getSessionForResource( return undefined; } +export type AgentFeedbackSessionChange = IChatSessionFileChange | IChatSessionFileChange2; + +export interface IAgentFeedbackContext { + readonly codeSelection?: string; + readonly diffHunks?: string; +} + +export function changeMatchesResource(change: AgentFeedbackSessionChange, resourceUri: URI): boolean { + if (isIChatSessionFileChange2(change)) { + return change.uri.fsPath === resourceUri.fsPath + || change.modifiedUri?.fsPath === resourceUri.fsPath + || change.originalUri?.fsPath === resourceUri.fsPath; + } + + return change.modifiedUri.fsPath === resourceUri.fsPath + || change.originalUri?.fsPath === resourceUri.fsPath; +} + +export function getSessionChangeForResource( + sessionResource: URI | undefined, + resourceUri: URI, + agentSessionsService: IAgentSessionsService, +): AgentFeedbackSessionChange | undefined { + if (!sessionResource) { + return undefined; + } + + const changes = agentSessionsService.getSession(sessionResource)?.changes; + if (!(changes instanceof Array)) { + return undefined; + } + + return changes.find(change => changeMatchesResource(change, resourceUri)); +} + +export function createAgentFeedbackContext( + editor: ICodeEditor, + codeEditorService: ICodeEditorService, + resourceUri: URI, + range: IRange, +): IAgentFeedbackContext { + const codeSelection = getCodeSelection(editor, codeEditorService, resourceUri, range); + const diffHunks = getDiffHunks(editor, codeEditorService, resourceUri, range); + return { codeSelection, diffHunks }; +} + +function getCodeSelection( + editor: ICodeEditor, + codeEditorService: ICodeEditorService, + resourceUri: URI, + range: IRange, +): string | undefined { + const model = getModelForResource(editor, codeEditorService, resourceUri); + if (!model) { + return undefined; + } + + const selection = model.getValueInRange(range); + return selection.length > 0 ? selection : undefined; +} + +function getDiffHunks( + editor: ICodeEditor, + codeEditorService: ICodeEditorService, + resourceUri: URI, + range: IRange, +): string | undefined { + const diffEditor = getContainingDiffEditor(editor, codeEditorService); + if (!diffEditor) { + return undefined; + } + + const originalModel = diffEditor.getOriginalEditor().getModel(); + const modifiedModel = diffEditor.getModifiedEditor().getModel(); + if (!originalModel || !modifiedModel) { + return undefined; + } + + const selectionIsInOriginal = isEqual(resourceUri, originalModel.uri); + const selectionIsInModified = isEqual(resourceUri, modifiedModel.uri); + if (!selectionIsInOriginal && !selectionIsInModified) { + return undefined; + } + + const diffResult = diffEditor.getDiffComputationResult(); + if (!diffResult) { + return undefined; + } + + const selectionIsEmpty = range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn; + const relevantGroups = groupChanges(diffResult.changes2).filter(group => { + const changeTouchesSelection = (change: DetailedLineRangeMapping) => rangeTouchesChange(range, selectionIsInOriginal ? change.original : change.modified); + return selectionIsEmpty ? group.some(changeTouchesSelection) : group.every(changeTouchesSelection); + }); + if (relevantGroups.length === 0) { + return undefined; + } + + const originalText = originalModel.getValue(); + const modifiedText = modifiedModel.getValue(); + const originalEndsWithNewline = originalText.length > 0 && originalText.endsWith('\n'); + const modifiedEndsWithNewline = modifiedText.length > 0 && modifiedText.endsWith('\n'); + const originalLines = originalText.split('\n'); + const modifiedLines = modifiedText.split('\n'); + + if (originalEndsWithNewline && originalLines[originalLines.length - 1] === '') { + originalLines.pop(); + } + if (modifiedEndsWithNewline && modifiedLines[modifiedLines.length - 1] === '') { + modifiedLines.pop(); + } + + return relevantGroups.map(group => renderHunkGroup(group, originalLines, modifiedLines, originalEndsWithNewline, modifiedEndsWithNewline)).join('\n'); +} + +function getContainingDiffEditor(editor: ICodeEditor, codeEditorService: ICodeEditorService): IDiffEditor | undefined { + return codeEditorService.listDiffEditors().find(diffEditor => + diffEditor.getModifiedEditor() === editor || diffEditor.getOriginalEditor() === editor + ); +} + +function getModelForResource(editor: ICodeEditor, codeEditorService: ICodeEditorService, resourceUri: URI) { + const currentModel = editor.getModel(); + if (currentModel && isEqual(currentModel.uri, resourceUri)) { + return currentModel; + } + + const diffEditor = getContainingDiffEditor(editor, codeEditorService); + const originalModel = diffEditor?.getOriginalEditor().getModel(); + if (originalModel && isEqual(originalModel.uri, resourceUri)) { + return originalModel; + } + + const modifiedModel = diffEditor?.getModifiedEditor().getModel(); + if (modifiedModel && isEqual(modifiedModel.uri, resourceUri)) { + return modifiedModel; + } + + return undefined; +} + +function groupChanges(changes: readonly DetailedLineRangeMapping[]): DetailedLineRangeMapping[][] { + const contextSize = 3; + const groups: DetailedLineRangeMapping[][] = []; + let currentGroup: DetailedLineRangeMapping[] = []; + + for (const change of changes) { + if (currentGroup.length === 0) { + currentGroup.push(change); + continue; + } + + const lastChange = currentGroup[currentGroup.length - 1]; + const lastContextEnd = lastChange.original.endLineNumberExclusive - 1 + contextSize; + const currentContextStart = change.original.startLineNumber - contextSize; + if (currentContextStart <= lastContextEnd + 1) { + currentGroup.push(change); + } else { + groups.push(currentGroup); + currentGroup = [change]; + } + } + + if (currentGroup.length > 0) { + groups.push(currentGroup); + } + + return groups; +} + +function rangeTouchesChange( + range: IRange, + lineRange: { startLineNumber: number; endLineNumberExclusive: number; isEmpty: boolean; contains(lineNumber: number): boolean }, +): boolean { + const isEmptySelection = range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn; + if (isEmptySelection) { + return !lineRange.isEmpty && lineRange.contains(range.startLineNumber); + } + + const selectionStart = range.startLineNumber; + const selectionEndExclusive = range.endLineNumber + 1; + return selectionStart <= lineRange.startLineNumber && lineRange.endLineNumberExclusive <= selectionEndExclusive; +} + +function renderHunkGroup( + group: readonly DetailedLineRangeMapping[], + originalLines: string[], + modifiedLines: string[], + originalEndsWithNewline: boolean, + modifiedEndsWithNewline: boolean, +): string { + const contextSize = 3; + const firstChange = group[0]; + const lastChange = group[group.length - 1]; + const hunkOrigStart = Math.max(1, firstChange.original.startLineNumber - contextSize); + const hunkOrigEnd = Math.min(originalLines.length, lastChange.original.endLineNumberExclusive - 1 + contextSize); + const hunkModStart = Math.max(1, firstChange.modified.startLineNumber - contextSize); + + const hunkLines: string[] = []; + let lastOriginalLineIndex = -1; + let lastModifiedLineIndex = -1; + let origLineNum = hunkOrigStart; + let origCount = 0; + let modCount = 0; + + for (const change of group) { + const origStart = change.original.startLineNumber; + const origEnd = change.original.endLineNumberExclusive; + const modStart = change.modified.startLineNumber; + const modEnd = change.modified.endLineNumberExclusive; + + while (origLineNum < origStart) { + const idx = hunkLines.length; + hunkLines.push(` ${originalLines[origLineNum - 1]}`); + if (origLineNum === originalLines.length) { + lastOriginalLineIndex = idx; + } + const modLineNum = hunkModStart + modCount; + if (modLineNum === modifiedLines.length) { + lastModifiedLineIndex = idx; + } + origLineNum++; + origCount++; + modCount++; + } + + for (let i = origStart; i < origEnd; i++) { + const idx = hunkLines.length; + hunkLines.push(`-${originalLines[i - 1]}`); + if (i === originalLines.length) { + lastOriginalLineIndex = idx; + } + origLineNum++; + origCount++; + } + + for (let i = modStart; i < modEnd; i++) { + const idx = hunkLines.length; + hunkLines.push(`+${modifiedLines[i - 1]}`); + if (i === modifiedLines.length) { + lastModifiedLineIndex = idx; + } + modCount++; + } + } + + while (origLineNum <= hunkOrigEnd) { + const idx = hunkLines.length; + hunkLines.push(` ${originalLines[origLineNum - 1]}`); + if (origLineNum === originalLines.length) { + lastOriginalLineIndex = idx; + } + const modLineNum = hunkModStart + modCount; + if (modLineNum === modifiedLines.length) { + lastModifiedLineIndex = idx; + } + origLineNum++; + origCount++; + modCount++; + } + + const header = `@@ -${hunkOrigStart},${origCount} +${hunkModStart},${modCount} @@`; + const result = [header, ...hunkLines]; + + if (!originalEndsWithNewline && lastOriginalLineIndex >= 0) { + result.splice(lastOriginalLineIndex + 2, 0, '\\ No newline at end of file'); + } else if (!modifiedEndsWithNewline && lastModifiedLineIndex >= 0) { + result.splice(lastModifiedLineIndex + 2, 0, '\\ No newline at end of file'); + } + + return result.join('\n'); +} + export function getActiveResourceCandidates(input: Parameters[0]): URI[] { const result: URI[] = []; const resources = EditorResourceAccessor.getOriginalUri(input, { supportSideBySide: SideBySideEditor.BOTH }); diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts index 6aed89f633b..197dc3504da 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.ts @@ -11,6 +11,7 @@ import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.j import { Event } from '../../../../base/common/event.js'; import { autorun, observableSignalFromEvent } from '../../../../base/common/observable.js'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { IEditorContribution, IEditorDecorationsCollection, ScrollType } from '../../../../editor/common/editorCommon.js'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; @@ -27,7 +28,7 @@ import { IAgentFeedbackService } from './agentFeedbackService.js'; import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js'; import { IChatSessionFileChange, IChatSessionFileChange2, isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; -import { getSessionForResource } from './agentFeedbackEditorUtils.js'; +import { createAgentFeedbackContext, getSessionForResource } from './agentFeedbackEditorUtils.js'; import { ICodeReviewService, IPRReviewState } from '../../codeReview/browser/codeReviewService.js'; import { getSessionEditorComments, groupNearbySessionEditorComments, ISessionEditorComment, SessionEditorCommentSource, toSessionEditorCommentId } from './sessionEditorComments.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; @@ -67,6 +68,7 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid @IAgentFeedbackService private readonly _agentFeedbackService: IAgentFeedbackService, @ICodeReviewService private readonly _codeReviewService: ICodeReviewService, @IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, ) { super(); @@ -298,7 +300,14 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid return; } - const feedback = this._agentFeedbackService.addFeedback(this._sessionResource, comment.resourceUri, comment.range, comment.text, comment.suggestion); + const feedback = this._agentFeedbackService.addFeedback( + this._sessionResource, + comment.resourceUri, + comment.range, + comment.text, + comment.suggestion, + createAgentFeedbackContext(this._editor, this._codeEditorService, comment.resourceUri, comment.range), + ); this._agentFeedbackService.setNavigationAnchor(this._sessionResource, toSessionEditorCommentId(SessionEditorCommentSource.AgentFeedback, feedback.id)); if (comment.source === SessionEditorCommentSource.CodeReview) { this._codeReviewService.removeComment(this._sessionResource, comment.sourceId); diff --git a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts index ea4414455e5..027e8ca5399 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts +++ b/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackHover.ts @@ -43,6 +43,7 @@ interface IFeedbackCommentElement { readonly text: string; readonly resourceUri: URI; readonly codeSelection?: string; + readonly diffHunks?: string; } type FeedbackTreeElement = IFeedbackFileElement | IFeedbackCommentElement; @@ -227,6 +228,11 @@ class FeedbackCommentRenderer implements ITreeRenderer; + addFeedbackAndSubmit(sessionResource: URI, resourceUri: URI, range: IRange, text: string, suggestion?: ICodeReviewSuggestion, context?: IAgentFeedbackContext): Promise; } // --- Implementation ----------------------------------------------------------- @@ -138,7 +141,7 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe super(); } - addFeedback(sessionResource: URI, resourceUri: URI, range: IRange, text: string, suggestion?: ICodeReviewSuggestion): IAgentFeedback { + addFeedback(sessionResource: URI, resourceUri: URI, range: IRange, text: string, suggestion?: ICodeReviewSuggestion, context?: IAgentFeedbackContext): IAgentFeedback { const key = sessionResource.toString(); let feedbackItems = this._feedbackBySession.get(key); if (!feedbackItems) { @@ -153,6 +156,8 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe range, sessionResource, suggestion, + codeSelection: context?.codeSelection, + diffHunks: context?.diffHunks, }; // Insert at the correct sorted position. @@ -414,8 +419,8 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe this._onDidChangeFeedback.fire({ sessionResource, feedbackItems: [] }); } - async addFeedbackAndSubmit(sessionResource: URI, resourceUri: URI, range: IRange, text: string, suggestion?: ICodeReviewSuggestion): Promise { - this.addFeedback(sessionResource, resourceUri, range, text, suggestion); + async addFeedbackAndSubmit(sessionResource: URI, resourceUri: URI, range: IRange, text: string, suggestion?: ICodeReviewSuggestion, context?: IAgentFeedbackContext): Promise { + this.addFeedback(sessionResource, resourceUri, range, text, suggestion, context); // Wait for the attachment contribution to update the chat widget's attachment model const widget = this._chatWidgetService.getWidgetBySessionResource(sessionResource); diff --git a/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts index 7818c7e172d..de89dd854bc 100644 --- a/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts +++ b/src/vs/sessions/contrib/agentFeedback/test/browser/agentFeedbackService.test.ts @@ -197,4 +197,14 @@ suite('AgentFeedbackService - Ordering', () => { assert.strictEqual(items[0].id, f1.id); assert.strictEqual(items[1].id, f2.id); }); + + test('preserves optional feedback context fields', () => { + const feedback = service.addFeedback(session, fileA, r(10), 'with context', undefined, { + codeSelection: 'const value = 1;', + diffHunks: '@@ -1,1 +1,1 @@\n-const value = 0;\n+const value = 1;', + }); + + assert.strictEqual(feedback.codeSelection, 'const value = 1;'); + assert.strictEqual(feedback.diffHunks, '@@ -1,1 +1,1 @@\n-const value = 0;\n+const value = 1;'); + }); }); diff --git a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts index f03eea0f2a1..0c7415c0533 100644 --- a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts +++ b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts @@ -306,6 +306,7 @@ export interface IAgentFeedbackVariableEntry extends IBaseChatRequestVariableEnt readonly resourceUri: URI; readonly range: IRange; readonly codeSelection?: string; + readonly diffHunks?: string; }>; }