chrome-devtools-frontend 1.0.1697595 → 1.0.1697998

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.stylelintignore +2 -0
  2. package/front_end/core/common/VersionController.ts +5 -1
  3. package/front_end/core/sdk/AnimationModel.ts +9 -1
  4. package/front_end/generated/InspectorBackendCommands.ts +1 -1
  5. package/front_end/generated/protocol.ts +8 -2
  6. package/front_end/models/ai_assistance/agents/ContextSelectionAgent.ts +26 -0
  7. package/front_end/models/ai_assistance/ai_assistance.ts +4 -4
  8. package/front_end/models/ai_assistance/skills/performance.md +4 -3
  9. package/front_end/models/ai_assistance/tools/{GetFunctionCode.ts → GetTraceFunctionCode.ts} +26 -13
  10. package/front_end/models/ai_assistance/tools/{GetResourceContent.ts → GetTraceResourceContent.ts} +31 -11
  11. package/front_end/models/ai_assistance/tools/Tool.ts +2 -2
  12. package/front_end/models/ai_assistance/tools/ToolRegistry.ts +6 -6
  13. package/front_end/models/elements/DOMChanges.ts +238 -0
  14. package/front_end/models/elements/elements.ts +2 -0
  15. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +3 -1
  16. package/front_end/panels/ai_assistance/components/MarkdownRendererWithCodeBlock.ts +25 -5
  17. package/front_end/panels/browser_debugger/ObjectEventListenersSidebarPane.ts +2 -3
  18. package/front_end/panels/elements/ElementsTreeElement.ts +98 -20
  19. package/front_end/panels/elements/ElementsTreeOutline.ts +271 -43
  20. package/front_end/panels/elements/EventListenersWidget.ts +0 -1
  21. package/front_end/panels/event_listeners/EventListenersView.ts +241 -297
  22. package/front_end/panels/linear_memory_inspector/LinearMemoryInspectorController.ts +1 -1
  23. package/front_end/panels/mobile_throttling/ThrottlingSettingsTab.ts +10 -2
  24. package/front_end/panels/network/NetworkConfigView.ts +141 -116
  25. package/front_end/panels/network/NetworkPanel.ts +7 -4
  26. package/front_end/panels/network/RequestHeadersView.ts +1 -1
  27. package/front_end/panels/network/forward/BackendLinking.ts +5 -2
  28. package/front_end/panels/settings/BackendLinkingSettingsTab.ts +7 -2
  29. package/front_end/panels/timeline/TimelineTreeView.ts +11 -1
  30. package/front_end/third_party/chromium/README.chromium +1 -1
  31. package/front_end/ui/comments/CommentOverlayManager.ts +169 -40
  32. package/front_end/ui/comments/CommentThreadWidget.ts +221 -0
  33. package/front_end/ui/comments/CommentsOverlayWidget.ts +124 -12
  34. package/front_end/ui/comments/commentThreadWidget.css +173 -0
  35. package/front_end/ui/comments/comments.ts +2 -0
  36. package/front_end/ui/comments/commentsOverlay.css +6 -0
  37. package/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts +1 -1
  38. package/front_end/ui/legacy/components/perf_ui/FlameChart.ts +4 -0
  39. package/package.json +5 -5
@@ -6,6 +6,7 @@ import * as Common from '../../core/common/common.js';
6
6
  import * as CommentManager from '../../models/comment_manager/comment_manager.js';
7
7
 
8
8
  import {
9
+ closestAcrossShadow,
9
10
  type CommentAnchorSignature,
10
11
  type CommentThread,
11
12
  computeVisibleRect,
@@ -25,7 +26,6 @@ export interface StartOptions {
25
26
  root?: Document|Element;
26
27
  scrollTarget?: EventTarget;
27
28
  resizeTarget?: Element;
28
- defaultText?: string;
29
29
  }
30
30
 
31
31
  export interface PinPositionData {
@@ -36,6 +36,8 @@ export interface PinPositionData {
36
36
  index: number;
37
37
  }
38
38
 
39
+ export type PendingPinPositionData = Omit<PinPositionData, 'id'>;
40
+
39
41
  export interface HighlightRectData {
40
42
  id: string;
41
43
  top: number;
@@ -45,6 +47,23 @@ export interface HighlightRectData {
45
47
  visible: boolean;
46
48
  }
47
49
 
50
+ export type PendingHighlightRectData = Omit<HighlightRectData, 'id'>;
51
+
52
+ export interface PendingDraft {
53
+ element: Element;
54
+ anchor: CommentAnchorSignature;
55
+ pin: PendingPinPositionData|null;
56
+ highlight: PendingHighlightRectData|null;
57
+ pinOffset: {offsetX: number, offsetY: number}|null;
58
+ }
59
+
60
+ export interface CreateCommentOptions {
61
+ author?: 'DEVELOPER'|'AGENT';
62
+ changes?: CommentManager.CommentManager.ChangeRecord[];
63
+ coordinates?: {clientX: number, clientY: number};
64
+ pendingDraft?: PendingDraft;
65
+ }
66
+
48
67
  export interface HoverHighlightData {
49
68
  top: number;
50
69
  left: number;
@@ -80,6 +99,7 @@ export class CommentOverlayManager extends Common.ObjectWrapper.ObjectWrapper<Ev
80
99
  #hoverData: HoverHighlightData|null = null;
81
100
  #pinPositions: PinPositionData[] = [];
82
101
  #highlightRects: HighlightRectData[] = [];
102
+ #pendingDraft: PendingDraft|null = null;
83
103
 
84
104
  #clickListener?: (event: Event) => void;
85
105
  #hoverListener?: (event: Event) => void;
@@ -129,6 +149,7 @@ export class CommentOverlayManager extends Common.ObjectWrapper.ObjectWrapper<Ev
129
149
  ({data: active}) => {
130
150
  if (!active) {
131
151
  this.#clearHover();
152
+ this.clearPendingAnchor();
132
153
  }
133
154
  document.body.style.cursor = active ? COMMENT_MODE_CURSOR : '';
134
155
  },
@@ -216,56 +237,123 @@ export class CommentOverlayManager extends Common.ObjectWrapper.ObjectWrapper<Ev
216
237
  return this.#highlightRects;
217
238
  }
218
239
 
219
- handleElementClick(
220
- element: Element,
221
- commentText = 'New comment',
222
- options?: {clientX: number, clientY: number},
223
- ): CommentThread|null {
224
- if (!this.isCommentMode()) {
225
- return null;
240
+ getPendingDraft(): PendingDraft|null {
241
+ return this.#pendingDraft;
242
+ }
243
+
244
+ clearPendingAnchor(): void {
245
+ if (!this.#pendingDraft) {
246
+ return;
247
+ }
248
+
249
+ if (!this.#observedThreads.has(this.#pendingDraft.element)) {
250
+ this.#intersectionObserver?.unobserve(this.#pendingDraft.element);
251
+ }
252
+ this.#pendingDraft = null;
253
+ this.#updatePositions();
254
+ }
255
+
256
+ handleElementClick(element: Element, options?: {clientX: number, clientY: number}): boolean {
257
+ if (!this.isCommentMode() || closestAcrossShadow(element, '.comment-thread-widget')) {
258
+ return false;
259
+ }
260
+ this.clearPendingAnchor();
261
+
262
+ const resolved = this.#resolveAnchor(element, options);
263
+ if (!resolved) {
264
+ return false;
265
+ }
266
+ const {anchor, anchorElement: anchorEl} = resolved;
267
+ const visibleRect = computeVisibleRect(anchorEl);
268
+ if (!visibleRect) {
269
+ return false;
270
+ }
271
+ const anchorRect = anchorEl.getBoundingClientRect();
272
+
273
+ let pinOffset: {offsetX: number, offsetY: number}|null = null;
274
+ if (options) {
275
+ pinOffset = {
276
+ offsetX: Math.min(Math.max(options.clientX, visibleRect.left), visibleRect.right) - anchorRect.left,
277
+ offsetY: Math.min(Math.max(options.clientY, visibleRect.top), visibleRect.bottom) - anchorRect.top,
278
+ };
279
+ }
280
+
281
+ this.#pendingDraft = {
282
+ element: anchorEl,
283
+ anchor,
284
+ pin: null,
285
+ highlight: null,
286
+ pinOffset,
287
+ };
288
+ const observer = this.#getIntersectionObserver();
289
+ if (!this.#observedThreads.has(anchorEl)) {
290
+ observer.observe(anchorEl);
226
291
  }
227
- return this.createComment(element, commentText, 'DEVELOPER', undefined, options);
292
+ this.#updatePositions();
293
+ return true;
228
294
  }
229
295
 
230
296
  createComment(
231
297
  element: Element,
232
298
  text: string,
233
- author: 'DEVELOPER'|'AGENT' = 'DEVELOPER',
234
- changes?: CommentManager.CommentManager.ChangeRecord[],
235
- options?: {clientX: number, clientY: number},
299
+ options?: CreateCommentOptions,
236
300
  ): CommentThread|null {
237
- let anchorEl: Element|null = null;
238
- let anchor: CommentAnchorSignature|null = null;
239
- const customResolver = getCustomAnchorResolverForElement(element);
240
- if (customResolver) {
241
- const result = customResolver.resolve(element, options);
242
- if (result) {
243
- anchor = result.anchor;
244
- anchorEl = result.anchorElement ?? element;
245
- }
301
+ const author = options?.author ?? 'DEVELOPER';
302
+ const changes = options?.changes;
303
+ const pendingDraft = options?.pendingDraft;
304
+ let resolved: {anchor: CommentAnchorSignature, anchorElement: Element}|null;
305
+ if (pendingDraft) {
306
+ resolved = {anchor: pendingDraft.anchor, anchorElement: pendingDraft.element};
246
307
  } else {
247
- anchorEl = resolveCommentAnchorElement(element, options);
248
- anchor = resolveCommentAnchor(element, undefined, options);
249
- }
250
- if (!anchor || !anchorEl) {
251
- return null;
308
+ resolved = this.#resolveAnchor(element, options?.coordinates);
309
+ if (!resolved) {
310
+ return null;
311
+ }
252
312
  }
253
313
 
314
+ const {anchor, anchorElement} = resolved;
315
+
254
316
  const thread = this.#commentManager.createCommentThread(anchor, text, author, changes);
255
317
  // Non-DOM anchors (e.g. canvas-rendered timeline entries) manage their own overlays
256
318
  // and do not have individual backing DOM nodes to cache or observe.
257
319
  if (isDomTrackedAnchor(anchor)) {
258
- this.#liveNodeCache.set(thread, anchorEl);
320
+ this.#liveNodeCache.set(thread, anchorElement);
259
321
 
260
322
  const observer = this.#getIntersectionObserver();
261
- observer.observe(anchorEl);
262
- this.#observedThreads.add(anchorEl);
323
+ observer.observe(anchorElement);
324
+ this.#observedThreads.add(anchorElement);
325
+ }
326
+
327
+ if (pendingDraft) {
328
+ this.clearPendingAnchor();
329
+ } else {
330
+ this.#updatePositions();
263
331
  }
264
332
 
265
- this.#updatePositions();
266
333
  return thread;
267
334
  }
268
335
 
336
+ #resolveAnchor(element: Element, options?: {clientX: number, clientY: number}):
337
+ {anchor: CommentAnchorSignature, anchorElement: Element}|null {
338
+ let anchorElement: Element|null = null;
339
+ let anchor: CommentAnchorSignature|null = null;
340
+ const customResolver = getCustomAnchorResolverForElement(element);
341
+ if (customResolver) {
342
+ const result = customResolver.resolve(element, options);
343
+ if (result) {
344
+ anchor = result.anchor;
345
+ anchorElement = result.anchorElement ?? element;
346
+ }
347
+ } else {
348
+ anchorElement = resolveCommentAnchorElement(element, options);
349
+ anchor = resolveCommentAnchor(element, undefined, options);
350
+ }
351
+ if (!anchor || !anchorElement) {
352
+ return null;
353
+ }
354
+ return {anchor, anchorElement};
355
+ }
356
+
269
357
  getCommentThread(id: string): CommentThread|undefined {
270
358
  return this.#commentManager.getCommentThread(id);
271
359
  }
@@ -405,12 +493,54 @@ export class CommentOverlayManager extends Common.ObjectWrapper.ObjectWrapper<Ev
405
493
 
406
494
  this.#pinPositions = newPins;
407
495
  this.#highlightRects = newHighlights;
496
+ this.#updatePendingPositions(scrollX, scrollY);
408
497
  this.dispatchEventToListeners(Events.POSITIONS_UPDATED, {
409
498
  pins: newPins,
410
499
  highlights: newHighlights,
411
500
  });
412
501
  }
413
502
 
503
+ #updatePendingPositions(scrollX: number, scrollY: number): void {
504
+ if (!this.#pendingDraft) {
505
+ return;
506
+ }
507
+
508
+ const visibleRect = computeVisibleRect(this.#pendingDraft.element);
509
+ if (!visibleRect) {
510
+ if (this.#pendingDraft.pin) {
511
+ this.#pendingDraft.pin.visible = false;
512
+ }
513
+ if (this.#pendingDraft.highlight) {
514
+ this.#pendingDraft.highlight.visible = false;
515
+ }
516
+ return;
517
+ }
518
+
519
+ const anchorRect = this.#pendingDraft.element.getBoundingClientRect();
520
+ const pinOffset = this.#pendingDraft.pinOffset;
521
+ const pinClientX = pinOffset ?
522
+ Math.min(Math.max(anchorRect.left + pinOffset.offsetX, visibleRect.left), visibleRect.right) :
523
+ visibleRect.right;
524
+ const pinClientY = pinOffset ?
525
+ Math.min(Math.max(anchorRect.top + pinOffset.offsetY, visibleRect.top), visibleRect.bottom) :
526
+ visibleRect.top;
527
+
528
+ this.#pendingDraft.pin = {
529
+ top: scrollY + pinClientY - 12,
530
+ left: scrollX + pinClientX - 12,
531
+ visible: true,
532
+ index: this.#commentManager.getCommentThreads().length + 1,
533
+ };
534
+
535
+ this.#pendingDraft.highlight = {
536
+ top: scrollY + visibleRect.top,
537
+ left: scrollX + visibleRect.left,
538
+ width: visibleRect.width,
539
+ height: visibleRect.height,
540
+ visible: true,
541
+ };
542
+ }
543
+
414
544
  /**
415
545
  * Initializes event listeners and lifecycle observers across the target DOM container.
416
546
  *
@@ -420,17 +550,15 @@ export class CommentOverlayManager extends Common.ObjectWrapper.ObjectWrapper<Ev
420
550
  * - A ResizeObserver to recalculate overlay coordinates when DevTools panels or drawers are resized.
421
551
  * - A MutationObserver to automatically rematch existing comment anchors when the DOM re-renders.
422
552
  */
423
- start(rootOrOptions?: Document|Element|StartOptions, defaultText = 'New comment'): void {
553
+ start(rootOrOptions?: Document|Element|StartOptions): void {
424
554
  let root: Document|Element|undefined;
425
555
  let scrollTarget: EventTarget|undefined;
426
556
  let resizeTarget: Element|undefined;
427
- let text = defaultText;
428
557
 
429
558
  if (rootOrOptions && !(rootOrOptions instanceof Document) && !(rootOrOptions instanceof Element)) {
430
559
  root = rootOrOptions.root;
431
560
  scrollTarget = rootOrOptions.scrollTarget;
432
561
  resizeTarget = rootOrOptions.resizeTarget;
433
- text = rootOrOptions.defaultText ?? defaultText;
434
562
  } else if (rootOrOptions) {
435
563
  root = rootOrOptions;
436
564
  }
@@ -440,7 +568,7 @@ export class CommentOverlayManager extends Common.ObjectWrapper.ObjectWrapper<Ev
440
568
  resizeTarget = resizeTarget || (root instanceof Document ? (root.body || root.documentElement) : root);
441
569
 
442
570
  this.stop();
443
- this.#installClickListener(root, text);
571
+ this.#installClickListener(root);
444
572
  this.#installScrollListener(scrollTarget);
445
573
  this.#installResizeObserver(resizeTarget);
446
574
  this.#installMutationObserver(root);
@@ -451,6 +579,7 @@ export class CommentOverlayManager extends Common.ObjectWrapper.ObjectWrapper<Ev
451
579
  */
452
580
  stop(): void {
453
581
  document.body.style.cursor = '';
582
+ this.clearPendingAnchor();
454
583
  this.#removeClickListener();
455
584
  this.#removeScrollListener();
456
585
  this.#removeResizeObserver();
@@ -465,12 +594,12 @@ export class CommentOverlayManager extends Common.ObjectWrapper.ObjectWrapper<Ev
465
594
  * Sets up capturing click, hover, and pointer interaction listeners on the container.
466
595
  *
467
596
  * When Comment Mode is active:
468
- * - Clicks on anchorable elements create new comment threads and consume the click event,
597
+ * - Clicks on anchorable elements open a comment thread creation draft and consume the click event,
469
598
  * preventing normal DevTools UI triggers such as node selection or navigation.
470
599
  * - Pointer and mouse press events are suppressed to prevent accidental text selections or drag interactions.
471
600
  * - Hover events compute and display a real-time preview highlight over the candidate anchor element.
472
601
  */
473
- #installClickListener(container: Element|Document = document, defaultText = 'New comment'): void {
602
+ #installClickListener(container: Element|Document = document): void {
474
603
  this.#removeClickListener();
475
604
  this.#clickContainer = container;
476
605
 
@@ -478,15 +607,15 @@ export class CommentOverlayManager extends Common.ObjectWrapper.ObjectWrapper<Ev
478
607
  if (!this.isCommentMode()) {
479
608
  return;
480
609
  }
610
+
481
611
  const composedTarget = event.composedPath()[0];
482
612
  const target = (composedTarget instanceof Element) ? composedTarget : event.target;
483
613
  if (!(target instanceof Element)) {
484
614
  return;
485
615
  }
486
- const mouseEvent = event as MouseEvent;
487
- const options = {clientX: mouseEvent.clientX, clientY: mouseEvent.clientY};
488
- const thread = this.handleElementClick(target, defaultText, options);
489
- if (thread) {
616
+
617
+ const options = event instanceof MouseEvent ? {clientX: event.clientX, clientY: event.clientY} : undefined;
618
+ if (this.handleElementClick(target, options)) {
490
619
  event.consume(true);
491
620
  }
492
621
  };
@@ -0,0 +1,221 @@
1
+ // Copyright 2026 The Chromium Authors
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+
5
+ import '../components/tooltips/tooltips.js';
6
+
7
+ import * as i18n from '../../core/i18n/i18n.js';
8
+ import type * as CommentManager from '../../models/comment_manager/comment_manager.js';
9
+ import * as UI from '../../ui/legacy/legacy.js';
10
+ import * as Lit from '../../ui/lit/lit.js';
11
+ import * as Input from '../components/input/input.js';
12
+ import * as MarkdownView from '../components/markdown_view/markdown_view.js';
13
+
14
+ import commentThreadWidgetStyles from './commentThreadWidget.css.js';
15
+
16
+ const {html, render, Directives: {createRef, ref}} = Lit;
17
+
18
+ const UIStrings = {
19
+ /**
20
+ * @description Link text in the info tooltip for learning more about comments sent to the agent.
21
+ */
22
+ learnMore: 'Learn more',
23
+ /**
24
+ * @description Text next to the checkmark in the comment thread header indicating that comments have been sent to
25
+ * the agent.
26
+ */
27
+ sent: 'Sent',
28
+ /**
29
+ * @description Alt text for the checkmark icon in the comment thread header indicating that comments have been sent
30
+ * to the agent.
31
+ */
32
+ sentCheckmark: 'Sent checkmark',
33
+ /**
34
+ * @description Label for the agent response section in the comment thread.
35
+ */
36
+ response: 'Response:',
37
+ /**
38
+ * @description Label for the aria-label of the add comment button.
39
+ */
40
+ addCommentButton: 'Add comment',
41
+ /**
42
+ * @description aria-label for the comment text area.
43
+ */
44
+ commentInputAriaLabel: 'Comment input',
45
+ } as const;
46
+
47
+ const UIStringsNotTranslate = {
48
+ /**
49
+ * @description Disclaimer text in the comment thread info tooltip.
50
+ */
51
+ inputDisclaimer:
52
+ 'Comment strings, DOM hierarchy snippets, tracked CSS and DOM changes, Visual Element (VE) paths and signatures, and tracked presenter changes are sent to the connected third-party agent to assist with debugging and code updates.',
53
+ } as const;
54
+
55
+ const str_ = i18n.i18n.registerUIStrings('ui/comments/CommentThreadWidget.ts', UIStrings);
56
+ const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
57
+ const lockedString = i18n.i18n.lockedString;
58
+
59
+ export interface ViewInput {
60
+ title: string;
61
+ comments: CommentManager.CommentManager.Comment[];
62
+ commentText: string;
63
+ textAreaRef: Lit.Directives.Ref<HTMLTextAreaElement>;
64
+ onLearnMoreClick: () => void;
65
+ onAddComment: (text: string) => void;
66
+ onCommentTextChange: (event: Event) => void;
67
+ }
68
+
69
+ export type ViewOutput = undefined;
70
+
71
+ export const DEFAULT_VIEW = (input: ViewInput, _output: ViewOutput, target: HTMLElement): void => {
72
+ const hasComment = input.comments.length > 0;
73
+
74
+ // clang-format off
75
+ render(html`
76
+ <style>${Input.textInputStyles}${commentThreadWidgetStyles}</style>
77
+ <div class="comment-thread-widget ${hasComment ? 'submitted' : ''}">
78
+ <div class="header">
79
+ <span class="selected-item">
80
+ <span class="selected-item-text">${input.title}</span>
81
+ </span>
82
+ ${hasComment ? html`
83
+ <div class="sent-status">
84
+ <devtools-icon
85
+ class="check-icon"
86
+ name="checkmark"
87
+ aria-label=${i18nString(UIStrings.sentCheckmark)}>
88
+ </devtools-icon>
89
+ <span>${i18nString(UIStrings.sent)}</span>
90
+ </div>
91
+ ` : Lit.nothing}
92
+ </div>
93
+
94
+ ${hasComment ? html`
95
+ ${input.comments.map(comment => comment.author === 'DEVELOPER' ? html`
96
+ <div class="comment-text">
97
+ ${comment.text}
98
+ </div>
99
+ ` : html`
100
+ <div class="agent-response">
101
+ <div class="elbow"></div>
102
+ <div class="response-content">
103
+ <div class="response-header">${i18nString(UIStrings.response)}</div>
104
+ ${MarkdownView.MarkdownView.renderTextAsMarkdown(
105
+ comment.text,
106
+ new MarkdownView.MarkdownView.MarkdownInsightRenderer(),
107
+ )}
108
+ </div>
109
+ </div>
110
+ `)}
111
+ ` : Lit.nothing}
112
+
113
+ ${!hasComment ? html`
114
+ <textarea
115
+ ${ref(input.textAreaRef)}
116
+ class="devtools-text-input input"
117
+ rows="1"
118
+ aria-label=${i18nString(UIStrings.commentInputAriaLabel)}
119
+ .value=${input.commentText}
120
+ @input=${input.onCommentTextChange}
121
+ @keydown=${(event: KeyboardEvent) => {
122
+ if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
123
+ event.preventDefault();
124
+ input.onAddComment(input.commentText);
125
+ }
126
+ }}
127
+ ></textarea>
128
+ <div class="footer">
129
+ <devtools-icon
130
+ class="info-icon"
131
+ name="info"
132
+ aria-label="Info"
133
+ aria-details="comment-thread-info-tooltip"
134
+ tabindex="0"
135
+ ></devtools-icon>
136
+ <devtools-tooltip
137
+ id="comment-thread-info-tooltip"
138
+ variant="rich"
139
+ >
140
+ <div class="info-tooltip-container">
141
+ ${lockedString(UIStringsNotTranslate.inputDisclaimer)}
142
+ <button
143
+ class="tooltip-link"
144
+ role="link"
145
+ @click=${input.onLearnMoreClick}
146
+ >${i18nString(UIStrings.learnMore)}</button>
147
+ </div>
148
+ </devtools-tooltip>
149
+ <devtools-button
150
+ aria-label=${i18nString(UIStrings.addCommentButton)}
151
+ .disabled=${!input.commentText.trim()}
152
+ @click=${() => input.onAddComment(input.commentText)}>
153
+ ${i18nString(UIStrings.addCommentButton)}
154
+ </devtools-button>
155
+ </div>
156
+ ` : Lit.nothing}
157
+ </div>
158
+ `, target);
159
+ // clang-format on
160
+ };
161
+
162
+ type View = typeof DEFAULT_VIEW;
163
+
164
+ export class CommentThreadWidget extends UI.Widget.Widget {
165
+ title = 'Comment Thread';
166
+ #comments: CommentManager.CommentManager.Comment[] = [];
167
+ #commentText = '';
168
+ #textAreaRef = createRef<HTMLTextAreaElement>();
169
+ #view: View;
170
+ onAddComment?: (text: string) => void;
171
+
172
+ constructor(element?: HTMLElement, view: View = DEFAULT_VIEW) {
173
+ super(element);
174
+ this.#view = view;
175
+ }
176
+
177
+ override wasShown(): void {
178
+ super.wasShown();
179
+ this.requestUpdate();
180
+ void this.updateComplete.then(() => {
181
+ this.#textAreaRef.value?.focus({preventScroll: true});
182
+ });
183
+ }
184
+
185
+ set comments(comments: CommentManager.CommentManager.Comment[]) {
186
+ this.#comments = comments;
187
+ this.requestUpdate();
188
+ }
189
+
190
+ #handleLearnMoreClick = (): void => {
191
+ // TODO: wire up the learn more click
192
+ };
193
+
194
+ #handleAddComment = (text: string): void => {
195
+ const commentText = text.trim();
196
+ if (commentText && this.onAddComment) {
197
+ this.onAddComment(commentText);
198
+ this.#commentText = '';
199
+ this.requestUpdate();
200
+ }
201
+ };
202
+
203
+ #handleCommentTextChange = (event: Event): void => {
204
+ const input = event.target as HTMLTextAreaElement;
205
+ this.#commentText = input.value;
206
+ this.requestUpdate();
207
+ };
208
+
209
+ override performUpdate(): void {
210
+ const viewInput: ViewInput = {
211
+ title: this.title,
212
+ comments: this.#comments,
213
+ commentText: this.#commentText,
214
+ textAreaRef: this.#textAreaRef,
215
+ onLearnMoreClick: this.#handleLearnMoreClick,
216
+ onAddComment: this.#handleAddComment,
217
+ onCommentTextChange: this.#handleCommentTextChange,
218
+ };
219
+ this.#view(viewInput, undefined, this.contentElement);
220
+ }
221
+ }