@@ -43,6 +43,7 @@ interface ICommentItemActions {
4343 editAction : Action ;
4444 convertAction : Action | undefined ;
4545 removeAction : Action ;
46+ addReplyAction : Action ;
4647}
4748
4849/**
@@ -60,6 +61,7 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid
6061 private readonly _toggleButton : HTMLElement ;
6162 private readonly _bodyNode : HTMLElement ;
6263 private readonly _itemElements = new Map < string , HTMLElement > ( ) ;
64+ private readonly _activeReplyInputs = new Map < string , { container : HTMLElement ; textarea : HTMLTextAreaElement } > ( ) ;
6365
6466 private _position : IOverlayWidgetPosition | null = null ;
6567 private _isExpanded : boolean = false ;
@@ -181,6 +183,7 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid
181183 private _buildFeedbackItems ( ) : void {
182184 clearNode ( this . _bodyNode ) ;
183185 this . _itemElements . clear ( ) ;
186+ this . _activeReplyInputs . clear ( ) ;
184187
185188 for ( const comment of this . _commentItems ) {
186189 const item = $ ( 'div.agent-feedback-widget-item' ) ;
@@ -212,7 +215,7 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid
212215 const actionBarContainer = $ ( 'div.agent-feedback-widget-item-actions' ) ;
213216 const actionBar = this . _eventStore . add ( new ActionBar ( actionBarContainer ) ) ;
214217
215- const itemActions : ICommentItemActions = { editAction : undefined ! , convertAction : undefined , removeAction : undefined ! } ;
218+ const itemActions : ICommentItemActions = { editAction : undefined ! , convertAction : undefined , removeAction : undefined ! , addReplyAction : undefined ! } ;
216219
217220 itemActions . editAction = this . _eventStore . add ( new Action (
218221 'agentFeedback.widget.edit' ,
@@ -223,6 +226,15 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid
223226 ) ) ;
224227 actionBar . push ( itemActions . editAction , { icon : true , label : false } ) ;
225228
229+ itemActions . addReplyAction = this . _eventStore . add ( new Action (
230+ 'agentFeedback.widget.addReply' ,
231+ nls . localize ( 'addToComment' , "Add to Comment" ) ,
232+ ThemeIcon . asClassName ( Codicon . commentDiscussion ) ,
233+ true ,
234+ ( ) : void => { this . _startAddingReply ( comment , item , itemActions ) ; } ,
235+ ) ) ;
236+ actionBar . push ( itemActions . addReplyAction , { icon : true , label : false } ) ;
237+
226238 if ( comment . canConvertToAgentFeedback ) {
227239 itemActions . convertAction = this . _eventStore . add ( new Action (
228240 'agentFeedback.widget.convert' ,
@@ -255,6 +267,10 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid
255267 item . appendChild ( this . _renderSuggestion ( comment ) ) ;
256268 }
257269
270+ if ( comment . replies ?. length ) {
271+ item . appendChild ( this . _renderReplies ( comment . replies ) ) ;
272+ }
273+
258274 this . _eventStore . add ( addDisposableListener ( item , 'mouseenter' , ( ) => {
259275 this . _highlightRange ( comment ) ;
260276 } ) ) ;
@@ -268,8 +284,12 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid
268284 if ( target ?. closest ( '.action-bar' ) ) {
269285 return ;
270286 }
287+ // Don't trigger navigation when interacting with the reply input.
288+ if ( target ?. closest ( '.agent-feedback-widget-add-reply' ) ) {
289+ return ;
290+ }
271291 // Don't navigate if the user just selected text inside the comment.
272- if ( target ?. closest ( '.agent-feedback-widget-text, .agent-feedback-widget-suggestion-text' ) ) {
292+ if ( target ?. closest ( '.agent-feedback-widget-text, .agent-feedback-widget-suggestion-text, .agent-feedback-widget-reply-text ' ) ) {
273293 const selection = this . _domNode . ownerDocument . defaultView ?. getSelection ( ) ;
274294 if ( selection && ! selection . isCollapsed && this . _domNode . contains ( selection . anchorNode ) ) {
275295 return ;
@@ -285,7 +305,7 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid
285305 // triggering the editor's copy action.
286306 const onSelectableMousedown = ( e : MouseEvent ) => {
287307 const target = e . target as HTMLElement | null ;
288- if ( target ?. closest ( '.agent-feedback-widget-text, .agent-feedback-widget-suggestion-text' ) ) {
308+ if ( target ?. closest ( '.agent-feedback-widget-text, .agent-feedback-widget-suggestion-text, .agent-feedback-widget-reply-text ' ) ) {
289309 this . _domNode . focus ( { preventScroll : true } ) ;
290310 }
291311 } ;
@@ -334,6 +354,22 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid
334354 return suggestionNode ;
335355 }
336356
357+ private _renderReplies ( replies : readonly string [ ] ) : HTMLElement {
358+ const repliesNode = $ ( 'div.agent-feedback-widget-replies' ) ;
359+
360+ for ( const reply of replies ) {
361+ const replyNode = $ ( 'div.agent-feedback-widget-reply' ) ;
362+ const replyText = $ ( 'div.agent-feedback-widget-reply-text' ) ;
363+ const rendered = this . _markdownRendererService . render ( new MarkdownString ( reply ) ) ;
364+ this . _eventStore . add ( rendered ) ;
365+ replyText . appendChild ( rendered . element ) ;
366+ replyNode . appendChild ( replyText ) ;
367+ repliesNode . appendChild ( replyNode ) ;
368+ }
369+
370+ return repliesNode ;
371+ }
372+
337373 private _removeComment ( comment : ISessionEditorComment ) : void {
338374 if ( comment . source === SessionEditorCommentSource . PRReview ) {
339375 this . _codeReviewService . resolvePRReviewThread ( this . _sessionResource ! , comment . sourceId ) ;
@@ -354,6 +390,7 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid
354390 actions . convertAction . enabled = false ;
355391 }
356392 actions . removeAction . enabled = false ;
393+ actions . addReplyAction . enabled = false ;
357394
358395 const editStore = new DisposableStore ( ) ;
359396 this . _eventStore . add ( editStore ) ;
@@ -400,6 +437,118 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid
400437 textarea . focus ( ) ;
401438 }
402439
440+ private _startAddingReply ( comment : ISessionEditorComment , itemNode : HTMLElement , actions : ICommentItemActions ) : void {
441+ // If a reply input is already open for this item, just focus it.
442+ const existing = this . _activeReplyInputs . get ( comment . id ) ;
443+ if ( existing ) {
444+ existing . textarea . focus ( ) ;
445+ return ;
446+ }
447+
448+ // Disable item actions while replying so the action bar doesn't conflict.
449+ actions . editAction . enabled = false ;
450+ if ( actions . convertAction ) {
451+ actions . convertAction . enabled = false ;
452+ }
453+ actions . removeAction . enabled = false ;
454+ actions . addReplyAction . enabled = false ;
455+
456+ const replyStore = new DisposableStore ( ) ;
457+ this . _eventStore . add ( replyStore ) ;
458+
459+ const replyContainer = $ ( 'div.agent-feedback-widget-add-reply' ) ;
460+ const textarea = $ ( 'textarea.agent-feedback-widget-edit-textarea' ) as HTMLTextAreaElement ;
461+ textarea . placeholder = nls . localize ( 'addReplyPlaceholder' , "Add a comment\u2026" ) ;
462+ textarea . rows = 1 ;
463+ replyContainer . appendChild ( textarea ) ;
464+ itemNode . appendChild ( replyContainer ) ;
465+ this . _activeReplyInputs . set ( comment . id , { container : replyContainer , textarea } ) ;
466+
467+ const autoSize = ( ) => {
468+ textarea . style . height = 'auto' ;
469+ textarea . style . height = `${ textarea . scrollHeight } px` ;
470+ this . _editor . layoutOverlayWidget ( this ) ;
471+ } ;
472+ autoSize ( ) ;
473+
474+ replyStore . add ( addDisposableListener ( textarea , 'input' , autoSize ) ) ;
475+
476+ const cleanup = ( ) => {
477+ replyStore . dispose ( ) ;
478+ actions . editAction . enabled = true ;
479+ if ( actions . convertAction ) {
480+ actions . convertAction . enabled = true ;
481+ }
482+ actions . removeAction . enabled = true ;
483+ actions . addReplyAction . enabled = true ;
484+ this . _activeReplyInputs . delete ( comment . id ) ;
485+ replyContainer . remove ( ) ;
486+ this . _editor . layoutOverlayWidget ( this ) ;
487+ } ;
488+
489+ replyStore . add ( addStandardDisposableListener ( textarea , 'keydown' , ( e ) => {
490+ if ( e . keyCode === KeyCode . Enter && ! e . shiftKey ) {
491+ e . preventDefault ( ) ;
492+ e . stopPropagation ( ) ;
493+ const newReply = textarea . value . trim ( ) ;
494+ if ( newReply ) {
495+ this . _saveReply ( comment , newReply ) ;
496+ // Widget will be rebuilt by the change event.
497+ } else {
498+ cleanup ( ) ;
499+ }
500+ } else if ( e . keyCode === KeyCode . Escape ) {
501+ e . preventDefault ( ) ;
502+ e . stopPropagation ( ) ;
503+ cleanup ( ) ;
504+ }
505+ } ) ) ;
506+
507+ // Cancel the reply when focus leaves and the textarea is empty.
508+ replyStore . add ( addDisposableListener ( textarea , 'blur' , ( ) => {
509+ if ( textarea . value . trim ( ) === '' ) {
510+ cleanup ( ) ;
511+ }
512+ } ) ) ;
513+
514+ textarea . focus ( ) ;
515+ }
516+
517+ private _saveReply ( comment : ISessionEditorComment , replyText : string ) : void {
518+ if ( comment . source === SessionEditorCommentSource . AgentFeedback ) {
519+ this . _agentFeedbackService . addReply ( this . _sessionResource , comment . sourceId , replyText ) ;
520+ return ;
521+ }
522+
523+ // For external comments (code review, PR review), convert to agent
524+ // feedback first preserving the original text, then add the reply so
525+ // that the original comment and the reply live in the same thread.
526+ if ( ! comment . canConvertToAgentFeedback ) {
527+ return ;
528+ }
529+
530+ const sourcePRReviewCommentId = comment . source === SessionEditorCommentSource . PRReview
531+ ? comment . sourceId
532+ : undefined ;
533+
534+ const feedback = this . _agentFeedbackService . addFeedback (
535+ this . _sessionResource ,
536+ comment . resourceUri ,
537+ comment . range ,
538+ comment . text ,
539+ comment . suggestion ,
540+ createAgentFeedbackContext ( this . _editor , this . _codeEditorService , comment . resourceUri , comment . range ) ,
541+ sourcePRReviewCommentId ,
542+ ) ;
543+ this . _agentFeedbackService . addReply ( this . _sessionResource , feedback . id , replyText ) ;
544+ this . _agentFeedbackService . setNavigationAnchor ( this . _sessionResource , toSessionEditorCommentId ( SessionEditorCommentSource . AgentFeedback , feedback . id ) ) ;
545+ if ( comment . source === SessionEditorCommentSource . CodeReview ) {
546+ this . _codeReviewService . removeComment ( this . _sessionResource , comment . sourceId ) ;
547+ } else if ( comment . source === SessionEditorCommentSource . PRReview ) {
548+ this . _codeReviewService . markPRReviewCommentConverted ( this . _sessionResource , comment . sourceId ) ;
549+ }
550+ }
551+
403552 private _saveEdit ( comment : ISessionEditorComment , newText : string ) : void {
404553 if ( comment . source === SessionEditorCommentSource . AgentFeedback ) {
405554 this . _agentFeedbackService . updateFeedback ( this . _sessionResource , comment . sourceId , newText ) ;
@@ -418,6 +567,7 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid
418567 actions . convertAction . enabled = true ;
419568 }
420569 actions . removeAction . enabled = true ;
570+ actions . addReplyAction . enabled = true ;
421571
422572 textContainer . classList . remove ( 'editing' ) ;
423573 clearNode ( textContainer ) ;
0 commit comments