mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-17 15:24:40 +01:00
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:
committed by
GitHub
parent
cff3949e5d
commit
f57e615389
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -306,6 +306,7 @@ export interface IAgentFeedbackVariableEntry extends IBaseChatRequestVariableEnt
|
||||
readonly resourceUri: URI;
|
||||
readonly range: IRange;
|
||||
readonly codeSelection?: string;
|
||||
readonly diffHunks?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user