Agent feedback editing (#302790)

* feat: add inline editing for agent feedback comments

* feat: enable editing for code review comments and add updateComment method

* feat: disable action bar actions while editing feedback comment

When inline editing is active on a feedback item, all action bar
buttons (edit, convert, remove) are disabled to prevent conflicting
operations. Actions are re-enabled when editing ends (Enter/Escape/blur).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Benjamin Christopher Simmonds
2026-03-18 13:56:52 +01:00
committed by GitHub
parent 452061cc22
commit 6dbea2838a
4 changed files with 185 additions and 5 deletions

View File

@@ -16,7 +16,7 @@ import { IEditorContribution, IEditorDecorationsCollection, ScrollType } from '.
import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js';
import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
import { $, addDisposableListener, clearNode, getTotalWidth } from '../../../../base/browser/dom.js';
import { $, addDisposableListener, addStandardDisposableListener, clearNode, getTotalWidth } from '../../../../base/browser/dom.js';
import { URI } from '../../../../base/common/uri.js';
import { Range } from '../../../../editor/common/core/range.js';
import { overviewRulerRangeHighlight } from '../../../../editor/common/core/editorColorRegistry.js';
@@ -36,6 +36,13 @@ import { isEqual } from '../../../../base/common/resources.js';
import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';
import { MarkdownString } from '../../../../base/common/htmlContent.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { KeyCode } from '../../../../base/common/keyCodes.js';
interface ICommentItemActions {
editAction: Action;
convertAction: Action | undefined;
removeAction: Action;
}
/**
* Widget that displays agent feedback comments for a group of nearby feedback items.
@@ -191,22 +198,42 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid
const actionBarContainer = $('div.agent-feedback-widget-item-actions');
const actionBar = this._eventStore.add(new ActionBar(actionBarContainer));
const itemActions: ICommentItemActions = { editAction: undefined!, convertAction: undefined, removeAction: undefined! };
// Edit action — only disabled for PR review comments
const isEditable = comment.source !== SessionEditorCommentSource.PRReview;
const editTooltip = isEditable
? nls.localize('editComment', "Edit")
: nls.localize('editPRCommentDisabled', "PR review comments cannot be edited");
itemActions.editAction = new Action(
'agentFeedback.widget.edit',
editTooltip,
ThemeIcon.asClassName(Codicon.edit),
isEditable,
(): void => { this._startEditing(comment, text, itemActions); },
);
actionBar.push(itemActions.editAction, { icon: true, label: false });
if (comment.canConvertToAgentFeedback) {
actionBar.push(new Action(
itemActions.convertAction = new Action(
'agentFeedback.widget.convert',
nls.localize('convertComment', "Convert to Agent Feedback"),
ThemeIcon.asClassName(Codicon.check),
true,
() => this._convertToAgentFeedback(comment),
), { icon: true, label: false });
);
actionBar.push(itemActions.convertAction, { icon: true, label: false });
}
actionBar.push(new Action(
itemActions.removeAction = new Action(
'agentFeedback.widget.remove',
nls.localize('removeComment', "Remove"),
ThemeIcon.asClassName(Codicon.close),
true,
() => this._removeComment(comment),
), { icon: true, label: false });
);
actionBar.push(itemActions.removeAction, { icon: true, label: false });
itemHeader.appendChild(actionBarContainer);
item.appendChild(itemHeader);
@@ -295,6 +322,89 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid
this._agentFeedbackService.removeFeedback(this._sessionResource, comment.sourceId);
}
private _startEditing(comment: ISessionEditorComment, textContainer: HTMLElement, actions: ICommentItemActions): void {
if (comment.source === SessionEditorCommentSource.PRReview) {
return;
}
// Disable all actions while editing
actions.editAction.enabled = false;
if (actions.convertAction) {
actions.convertAction.enabled = false;
}
actions.removeAction.enabled = false;
const editStore = new DisposableStore();
this._eventStore.add(editStore);
clearNode(textContainer);
textContainer.classList.add('editing');
const textarea = $('textarea.agent-feedback-widget-edit-textarea') as HTMLTextAreaElement;
textarea.value = comment.text;
textarea.rows = 1;
textContainer.appendChild(textarea);
// Auto-size the textarea
const autoSize = () => {
textarea.style.height = 'auto';
textarea.style.height = `${textarea.scrollHeight}px`;
this._editor.layoutOverlayWidget(this);
};
autoSize();
editStore.add(addDisposableListener(textarea, 'input', autoSize));
editStore.add(addStandardDisposableListener(textarea, 'keydown', (e) => {
if (e.keyCode === KeyCode.Enter && !e.shiftKey) {
e.preventDefault();
e.stopPropagation();
const newText = textarea.value.trim();
if (newText) {
this._saveEdit(comment, newText);
}
// Widget will be rebuilt by the change event
} else if (e.keyCode === KeyCode.Escape) {
e.preventDefault();
e.stopPropagation();
this._stopEditing(comment, textContainer, editStore, actions);
}
}));
// Stop editing when focus is lost
editStore.add(addDisposableListener(textarea, 'blur', () => {
this._stopEditing(comment, textContainer, editStore, actions);
}));
textarea.focus();
}
private _saveEdit(comment: ISessionEditorComment, newText: string): void {
if (comment.source === SessionEditorCommentSource.AgentFeedback) {
this._agentFeedbackService.updateFeedback(this._sessionResource, comment.sourceId, newText);
} else if (comment.source === SessionEditorCommentSource.CodeReview) {
this._codeReviewService.updateComment(this._sessionResource, comment.sourceId, newText);
}
}
private _stopEditing(comment: ISessionEditorComment, textContainer: HTMLElement, editStore: DisposableStore, actions: ICommentItemActions): void {
editStore.dispose();
// Re-enable actions
actions.editAction.enabled = comment.source !== SessionEditorCommentSource.PRReview;
if (actions.convertAction) {
actions.convertAction.enabled = true;
}
actions.removeAction.enabled = true;
textContainer.classList.remove('editing');
clearNode(textContainer);
const rendered = this._markdownRendererService.render(new MarkdownString(comment.text));
this._eventStore.add(rendered);
textContainer.appendChild(rendered.element);
this._editor.layoutOverlayWidget(this);
}
private _convertToAgentFeedback(comment: ISessionEditorComment): void {
if (!comment.canConvertToAgentFeedback) {
return;

View File

@@ -68,6 +68,11 @@ export interface IAgentFeedbackService {
*/
removeFeedback(sessionResource: URI, feedbackId: string): void;
/**
* Update the text of an existing feedback item.
*/
updateFeedback(sessionResource: URI, feedbackId: string, newText: string): void;
/**
* Get all feedback items for a session.
*/
@@ -220,6 +225,25 @@ export class AgentFeedbackService extends Disposable implements IAgentFeedbackSe
}
}
updateFeedback(sessionResource: URI, feedbackId: string, newText: string): void {
const key = sessionResource.toString();
const feedbackItems = this._feedbackBySession.get(key);
if (!feedbackItems) {
return;
}
const idx = feedbackItems.findIndex(f => f.id === feedbackId);
if (idx >= 0) {
const existing = feedbackItems[idx];
feedbackItems[idx] = {
...existing,
text: newText,
};
this._sessionUpdatedOrder.set(key, ++this._sessionUpdatedSequence);
this._onDidChangeFeedback.fire({ sessionResource, feedbackItems });
}
}
getFeedback(sessionResource: URI): readonly IAgentFeedback[] {
return this._feedbackBySession.get(sessionResource.toString()) ?? [];
}

View File

@@ -286,3 +286,28 @@
z-index: 5;
border-left: 2px solid var(--vscode-editorGutter-modifiedBackground);
}
/* Inline edit textarea */
.agent-feedback-widget-text.editing {
padding: 0;
}
.agent-feedback-widget-edit-textarea {
width: 100%;
min-height: 22px;
padding: 4px 6px;
border: 1px solid var(--vscode-focusBorder);
border-radius: 4px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
font-size: 12px;
line-height: 1.4;
resize: none;
overflow: hidden;
box-sizing: border-box;
}
.agent-feedback-widget-edit-textarea:focus {
outline: none;
border-color: var(--vscode-focusBorder);
}

View File

@@ -178,6 +178,11 @@ export interface ICodeReviewService {
*/
removeComment(sessionResource: URI, commentId: string): void;
/**
* Update the body text of a single code review comment.
*/
updateComment(sessionResource: URI, commentId: string, newBody: string): void;
/**
* Dismiss/clear the review for a session entirely.
*/
@@ -372,6 +377,22 @@ export class CodeReviewService extends Disposable implements ICodeReviewService
this._saveToStorage();
}
updateComment(sessionResource: URI, commentId: string, newBody: string): void {
const data = this._reviewsBySession.get(sessionResource.toString());
if (!data) {
return;
}
const state = data.state.get();
if (state.kind !== CodeReviewStateKind.Result) {
return;
}
const updated = state.comments.map(c => c.id === commentId ? { ...c, body: newBody } : c);
data.state.set({ kind: CodeReviewStateKind.Result, version: state.version, comments: updated }, undefined);
this._saveToStorage();
}
dismissReview(sessionResource: URI): void {
const data = this._reviewsBySession.get(sessionResource.toString());
if (data) {