Agent feedback: show input on diff hunk click and include diff context in attachments (#302401)

* Enhance agent feedback editor: add diff editor support and improve selection handling

* Prevent focus shift on navigation keys in agent feedback editor

* Enhance agent feedback: add support for diff hunks and update related interfaces

* Fix range comparison logic in _rangeTouchesChange method

* Refactor agent feedback context handling: streamline feedback addition and enhance context management

* Enhance hover behavior: add persistence option to prevent hiding on hover

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Benjamin Christopher Simmonds
2026-03-17 12:15:57 +01:00
committed by GitHub
parent cff3949e5d
commit f57e615389
8 changed files with 425 additions and 73 deletions

View File

@@ -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<string>());
/** Cache of resolved code snippets keyed by feedback ID */
private readonly _snippetCache = new Map<string, string | undefined>();
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<string> {
// 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<string | undefined> {
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.
*/

View File

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

View File

@@ -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<typeof EditorResourceAccessor.getOriginalUri>[0]): URI[] {
const result: URI[] = [];
const resources = EditorResourceAccessor.getOriginalUri(input, { supportSideBySide: SideBySideEditor.BOTH });

View File

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

View File

@@ -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<IFeedbackCommentElement,
markdown.appendCodeblock(languageId ?? '', element.codeSelection);
}
if (element.diffHunks) {
markdown.appendMarkdown('\n\n');
markdown.appendCodeblock('diff', element.diffHunks);
}
return {
content: markdown,
style: HoverStyle.Pointer,
@@ -351,6 +357,7 @@ export class AgentFeedbackHover extends Disposable {
return {
content: hoverElement,
style: HoverStyle.Pointer,
persistence: { hideOnHover: false },
position: { hoverPosition: HoverPosition.ABOVE },
trapFocus: true,
appearance: { compact: true },
@@ -376,6 +383,7 @@ export class AgentFeedbackHover extends Disposable {
text: item.text,
resourceUri: item.resourceUri,
codeSelection: item.codeSelection,
diffHunks: item.diffHunks,
});
}

View File

@@ -19,6 +19,7 @@ import { IChatWidgetService } from '../../../../workbench/contrib/chat/browser/c
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { ICodeReviewSuggestion } from '../../codeReview/browser/codeReviewService.js';
import { IAgentFeedbackContext } from './agentFeedbackEditorUtils.js';
// --- Types --------------------------------------------------------------------
@@ -29,6 +30,8 @@ export interface IAgentFeedback {
readonly range: IRange;
readonly sessionResource: URI;
readonly suggestion?: ICodeReviewSuggestion;
readonly codeSelection?: string;
readonly diffHunks?: string;
}
export interface INavigableSessionComment {
@@ -58,7 +61,7 @@ export interface IAgentFeedbackService {
/**
* Add a feedback item for the given session.
*/
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;
/**
* Remove a single feedback item.
@@ -107,7 +110,7 @@ export interface IAgentFeedbackService {
* Add a feedback item and then submit the feedback. Waits for the
* attachment to be updated in the chat widget before submitting.
*/
addFeedbackAndSubmit(sessionResource: URI, resourceUri: URI, range: IRange, text: string, suggestion?: ICodeReviewSuggestion): Promise<void>;
addFeedbackAndSubmit(sessionResource: URI, resourceUri: URI, range: IRange, text: string, suggestion?: ICodeReviewSuggestion, context?: IAgentFeedbackContext): Promise<void>;
}
// --- 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<void> {
this.addFeedback(sessionResource, resourceUri, range, text, suggestion);
async addFeedbackAndSubmit(sessionResource: URI, resourceUri: URI, range: IRange, text: string, suggestion?: ICodeReviewSuggestion, context?: IAgentFeedbackContext): Promise<void> {
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);

View File

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

View File

@@ -306,6 +306,7 @@ export interface IAgentFeedbackVariableEntry extends IBaseChatRequestVariableEnt
readonly resourceUri: URI;
readonly range: IRange;
readonly codeSelection?: string;
readonly diffHunks?: string;
}>;
}