mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-17 15:24:40 +01:00
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:
committed by
GitHub
parent
452061cc22
commit
6dbea2838a
@@ -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;
|
||||
|
||||
@@ -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()) ?? [];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user