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
@@ -337,7 +337,9 @@ function getMarkdownRenderer(conversation?: AiAssistanceModel.AiConversation.AiC
337
337
  return new AccessibilityAgentMarkdownRenderer(mainDocumentURL);
338
338
  }
339
339
 
340
- return new MarkdownRendererWithCodeBlock();
340
+ return new MarkdownRendererWithCodeBlock({
341
+ getEstablishedOrigin: () => conversation?.origin,
342
+ });
341
343
  }
342
344
 
343
345
  interface ToolbarViewInput {
@@ -4,6 +4,7 @@
4
4
 
5
5
  import * as Common from '../../../core/common/common.js';
6
6
  import * as Platform from '../../../core/platform/platform.js';
7
+ import type * as SDK from '../../../core/sdk/sdk.js';
7
8
  import * as AiAssistanceModel from '../../../models/ai_assistance/ai_assistance.js';
8
9
  import * as Logs from '../../../models/logs/logs.js';
9
10
  import type * as Marked from '../../../third_party/marked/marked.js';
@@ -12,7 +13,18 @@ import * as Lit from '../../../ui/lit/lit.js';
12
13
 
13
14
  const {html} = Lit;
14
15
 
16
+ export interface MarkdownRendererWithCodeBlockOptions {
17
+ /**
18
+ * Retrieves the established origin locked for the active conversation.
19
+ * Required to authorize #file-<id> links and prevent cross-origin file leakage.
20
+ * When omitted or unauthorized, file links safely render as plain inert text.
21
+ */
22
+ getEstablishedOrigin?: () => SDK.SecurityOrigin.SecurityOrigin | undefined;
23
+ }
24
+
15
25
  /**
26
+ * Markdown renderer for AI assistance conversations.
27
+ *
16
28
  * The model returns multiline code blocks in an erroneous way with the language being in new line.
17
29
  * This renderer takes that into account and correctly updates the parsed multiline token with the language
18
30
  * correctly identified and stripped from the content.
@@ -23,8 +35,13 @@ const {html} = Lit;
23
35
  * color: red;
24
36
  * }
25
37
  * ```
26
- **/
38
+ *
39
+ * Also handles linkifying DevTools internal resource links (#req- and origin-locked #file-).
40
+ */
27
41
  export class MarkdownRendererWithCodeBlock extends MarkdownView.MarkdownView.MarkdownInsightRenderer {
42
+ constructor(private readonly options: MarkdownRendererWithCodeBlockOptions = {}) {
43
+ super();
44
+ }
28
45
  #revealableLink(revealable: unknown, label: string): Lit.LitTemplate {
29
46
  return html`<devtools-link @click=${(e: Event) => {
30
47
  e.preventDefault();
@@ -48,13 +65,16 @@ export class MarkdownRendererWithCodeBlock extends MarkdownView.MarkdownView.Mar
48
65
  return html`${fallbackText}`;
49
66
  }
50
67
  if (href.startsWith('#file-')) {
51
- const file = AiAssistanceModel.ContextSelectionAgent.ContextSelectionAgent.getUISourceCodes().find(
52
- file => AiAssistanceModel.ContextSelectionAgent.ContextSelectionAgent.uiSourceCodeId.get(file) ===
53
- Number(href.substring(6)));
68
+ const fileId = Number(href.substring(6));
69
+ const file = AiAssistanceModel.ContextSelectionAgent.ContextSelectionAgent.getSourceById(
70
+ fileId,
71
+ this.options.getEstablishedOrigin?.(),
72
+ );
54
73
 
55
74
  if (file) {
56
75
  return this.#revealableLink(file, file.name());
57
76
  }
77
+ // Fall back to plain text to prevent rendering unauthorized or unverified links.
58
78
  return html`${fallbackText}`;
59
79
  }
60
80
  return null;
@@ -78,7 +98,7 @@ export class MarkdownRendererWithCodeBlock extends MarkdownView.MarkdownView.Mar
78
98
 
79
99
  if (token.type === 'codespan') {
80
100
  // LLM likes outputting the link inside a codespan block.
81
- // Remove the codespan and render the link directly
101
+ // Remove the codespan and render the link directly.
82
102
  const matches = token.text.match(/^\[(.*)\]\((.+)\)$/);
83
103
  if (matches?.[2]) {
84
104
  const link = this.#renderLink(
@@ -20,8 +20,6 @@ export class ObjectEventListenersSidebarPane extends UI.Widget.VBox implements U
20
20
  this.contentElement.setAttribute('jslog', `${VisualLogging.section('sources.global-listeners')}`);
21
21
 
22
22
  this.eventListenersView = new EventListeners.EventListenersView.EventListenersView();
23
- this.eventListenersView.changeCallback = this.requestUpdate.bind(this);
24
- this.eventListenersView.enableDefaultTreeFocus = true;
25
23
  this.eventListenersView.show(this.element);
26
24
  this.setDefaultFocusedChild(this.eventListenersView);
27
25
  this.requestUpdate();
@@ -57,7 +55,8 @@ export class ObjectEventListenersSidebarPane extends UI.Widget.VBox implements U
57
55
  windowObjects.push(result.object);
58
56
  }
59
57
  }
60
- await this.eventListenersView.addObjects(windowObjects);
58
+ this.eventListenersView.objects = windowObjects;
59
+ await this.eventListenersView.updateComplete;
61
60
  }
62
61
 
63
62
  override wasShown(): void {
@@ -48,7 +48,8 @@ import * as Protocol from '../../generated/protocol.js';
48
48
  import * as AIAssistance from '../../models/ai_assistance/ai_assistance.js';
49
49
  import * as Badges from '../../models/badges/badges.js';
50
50
  import * as Bindings from '../../models/bindings/bindings.js';
51
- import type * as Elements from '../../models/elements/elements.js';
51
+ import * as ChangeTracker from '../../models/change_tracker/change_tracker.js';
52
+ import * as Elements from '../../models/elements/elements.js';
52
53
  import * as IssuesManager from '../../models/issues_manager/issues_manager.js';
53
54
  import * as Workspace from '../../models/workspace/workspace.js';
54
55
  import * as CodeMirror from '../../third_party/codemirror.next/codemirror.next.js';
@@ -65,6 +66,7 @@ import * as PanelsCommon from '../common/common.js';
65
66
  import * as Media from '../media/media.js';
66
67
 
67
68
  import * as ElementsComponents from './components/components.js';
69
+ import {cssPath} from './DOMPath.js';
68
70
  import {getElementIssueDetails} from './ElementIssueUtils.js';
69
71
  import {ElementsPanel} from './ElementsPanel.js';
70
72
  import * as ElementStatePaneWidget from './ElementStatePaneWidget.js';
@@ -72,6 +74,33 @@ import {type ElementsTreeOutline, MappedCharToEntity} from './ElementsTreeOutlin
72
74
  import {ImagePreviewPopover} from './ImagePreviewPopover.js';
73
75
  import {getRegisteredDecorators, type MarkerDecorator, type MarkerDecoratorRegistration} from './MarkerDecorator.js';
74
76
 
77
+ /**
78
+ * Returns the CSS selector of `node` that is used as the text signature of the anchor a change is
79
+ * recorded on, or `undefined` if `tracker` is absent or not recording changes.
80
+ *
81
+ * Resolving the selector walks the ancestor chain and scans sibling lists, so it is skipped
82
+ * entirely while change tracking is disabled.
83
+ */
84
+ export function buildChangeSelector(tracker: ChangeTracker.ChangeTracker.ChangeTracker|undefined,
85
+ node: SDK.DOMModel.DOMNode): string|undefined {
86
+ if (!tracker?.isTracking) {
87
+ return undefined;
88
+ }
89
+ if (node.nodeType() === Node.ELEMENT_NODE) {
90
+ const selector = cssPath(node, true);
91
+ if (selector) {
92
+ return selector;
93
+ }
94
+ } else if (node.parentNode && node.parentNode.nodeType() === Node.ELEMENT_NODE) {
95
+ const selector = cssPath(node.parentNode, true);
96
+ if (selector) {
97
+ return selector;
98
+ }
99
+ }
100
+
101
+ return undefined;
102
+ }
103
+
75
104
  const {html, nothing, render, Directives: {classMap, ref, repeat, until}} = Lit;
76
105
  const {animateOn} = UI.UIUtils;
77
106
 
@@ -770,14 +799,6 @@ function renderTag(node: SDK.DOMModel.DOMNode, tagName: string, isClosingTag: bo
770
799
  hasUpdates = hasUpdates || (!expanded && updateRecord.hasChangedChildren());
771
800
  }
772
801
 
773
- // We are taking full text content of the tag, including attributes and children, to set the aria label.
774
- // FIXME: we should compute the aria label ourselves if it is event needed.
775
- const setAriaLabel = ref(el => {
776
- if (el?.textContent) {
777
- UI.ARIAUtils.setLabel(el, el.textContent);
778
- }
779
- });
780
-
781
802
  const tagNameClass = isClosingTag ? 'webkit-html-close-tag-name' : 'webkit-html-tag-name';
782
803
  const hasTagIssues = !isClosingTag && Boolean(issues?.some(issue => {
783
804
  const details = getElementIssueDetails(issue);
@@ -790,8 +811,13 @@ function renderTag(node: SDK.DOMModel.DOMNode, tagName: string, isClosingTag: bo
790
811
  const tagString = (isClosingTag ? '/' : '') + tagName;
791
812
  const jslog = !isClosingTag ? VisualLogging.value('tag-name').track({change: true, dblclick: true}) : '';
792
813
 
814
+ const ariaLabel = isClosingTag ?
815
+ `</${tagName}>` :
816
+ `<${tagName}${
817
+ attributes.map(attr => (attr.value ? ` ${attr.name}="${attr.value}"` : ` ${attr.name}`)).join('')}>`;
818
+
793
819
  return html`<span
794
- class=${classMap(tagClasses)} ${setAriaLabel}
820
+ class=${classMap(tagClasses)} aria-label=${ariaLabel}
795
821
  >&lt;<span class=${classMap(tagNameClasses)} jslog=${jslog || nothing} ${
796
822
  animateOn(hasUpdates, DOM_UPDATE_ANIMATION_CLASS_NAME)}>${tagString}</span>${
797
823
  attributes.map(attr => html` ${renderAttribute(attr, updateRecord, false, node, issues)}`)}&gt;</span>\u200B`;
@@ -1102,7 +1128,10 @@ export const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLE
1102
1128
  if (event.key === 'Escape') {
1103
1129
  event.consume(true);
1104
1130
  }
1105
- }} class="source-code elements-tree-editor" style="width: ${input.editorWidth ?? 0}px;">
1131
+ }}
1132
+ @mousedown=${(event: Event) => event.stopPropagation()}
1133
+ @click=${(event: Event) => event.stopPropagation()}
1134
+ class="source-code elements-tree-editor" style="width: ${input.editorWidth ?? 0}px;">
1106
1135
  <devtools-text-editor .state=${input.editorState} ${ref(el => {
1107
1136
  output.editorRef = el as TextEditor.TextEditor.TextEditor;
1108
1137
  })}></devtools-text-editor>
@@ -1124,9 +1153,14 @@ export interface InitialEditState {
1124
1153
  }
1125
1154
 
1126
1155
  export class ElementsTreeWidget extends UI.Widget.Widget {
1127
- static override readonly INJECT: readonly[typeof IssuesManager.DOMIssuesManager.DOMIssuesManager] =
1128
- [IssuesManager.DOMIssuesManager.DOMIssuesManager] as const;
1156
+ static override readonly INJECT: readonly[typeof IssuesManager.DOMIssuesManager.DOMIssuesManager,
1157
+ typeof ChangeTracker.ChangeTracker.ChangeTracker] =
1158
+ [
1159
+ IssuesManager.DOMIssuesManager.DOMIssuesManager,
1160
+ ChangeTracker.ChangeTracker.ChangeTracker,
1161
+ ] as const;
1129
1162
  #domIssuesManager?: IssuesManager.DOMIssuesManager.DOMIssuesManager;
1163
+ #changeTracker?: ChangeTracker.ChangeTracker.ChangeTracker;
1130
1164
 
1131
1165
  #node!: SDK.DOMModel.DOMNode;
1132
1166
  #eventsBound = false;
@@ -1370,13 +1404,25 @@ export class ElementsTreeWidget extends UI.Widget.Widget {
1370
1404
  return this.#domIssuesManager?.issuesForNode(this.node) ?? [];
1371
1405
  }
1372
1406
 
1407
+ get changeTracker(): ChangeTracker.ChangeTracker.ChangeTracker|undefined {
1408
+ // The widget is currently created for non-widget ElementsTreeElement so
1409
+ // the changeTracker can be empty, relying on the manual resolution.
1410
+ // Note that this only works once the widget is attached to the DOM.
1411
+ this.#changeTracker ??=
1412
+ UI.Widget.lookupUniverseForElement(this.contentElement)?.get(ChangeTracker.ChangeTracker.ChangeTracker);
1413
+ return this.#changeTracker;
1414
+ }
1415
+
1373
1416
  constructor(
1374
1417
  element?: HTMLElement,
1375
- [domIssuesManager]: UI.Widget.WidgetDependencies<typeof ElementsTreeWidget>|[undefined] = [undefined],
1418
+ [domIssuesManager, changeTracker]:
1419
+ UI.Widget.WidgetDependencies<typeof ElementsTreeWidget>|[IssuesManager.DOMIssuesManager.DOMIssuesManager?,
1420
+ ChangeTracker.ChangeTracker.ChangeTracker?] = [],
1376
1421
  view: View = DEFAULT_VIEW,
1377
1422
  ) {
1378
1423
  super(element);
1379
1424
  this.#domIssuesManager = domIssuesManager;
1425
+ this.#changeTracker = changeTracker;
1380
1426
  this.#view = view;
1381
1427
 
1382
1428
  this.#expandedChildrenLimit = InitialChildrenLimit;
@@ -1461,6 +1507,7 @@ export class ElementsTreeWidget extends UI.Widget.Widget {
1461
1507
  }
1462
1508
 
1463
1509
  #clearDOMNextUpdate = false;
1510
+
1464
1511
  #isBound = false;
1465
1512
 
1466
1513
  override wasShown(): void {
@@ -2448,7 +2495,8 @@ export class ElementsTreeWidget extends UI.Widget.Widget {
2448
2495
  }
2449
2496
  // The relatedTarget is null when no element gains focus, e.g. switching windows.
2450
2497
  const relatedTarget = (event.relatedTarget as Node | null);
2451
- if (relatedTarget && !relatedTarget.isSelfOrDescendant(this.#editorRef)) {
2498
+ if (relatedTarget && !relatedTarget.isSelfOrDescendant(this.#editorRef) &&
2499
+ !relatedTarget.isSelfOrDescendant(this.contentElement)) {
2452
2500
  this.editing?.commit();
2453
2501
  }
2454
2502
  },
@@ -2595,10 +2643,16 @@ export class ElementsTreeWidget extends UI.Widget.Widget {
2595
2643
  }
2596
2644
 
2597
2645
  if (attributeName !== null && (attributeName.trim() || newText.trim()) && oldText !== newText) {
2646
+ const edit = {attributeName, oldText, newText};
2598
2647
  this.node.setAttribute(attributeName, newText, (error: string|null) => {
2648
+ if (!error) {
2649
+ const changeTracker = this.changeTracker;
2650
+ Elements.DOMChanges.trackAttributeEdit(changeTracker, this.node,
2651
+ buildChangeSelector(changeTracker, this.node), edit);
2652
+ Badges.UserBadges.instance().recordAction(Badges.BadgeAction.DOM_ELEMENT_OR_ATTRIBUTE_EDITED);
2653
+ }
2599
2654
  moveToNextAttributeIfNeeded.call(this, error);
2600
2655
  });
2601
- Badges.UserBadges.instance().recordAction(Badges.BadgeAction.DOM_ELEMENT_OR_ATTRIBUTE_EDITED);
2602
2656
  return;
2603
2657
  }
2604
2658
 
@@ -2660,6 +2714,9 @@ export class ElementsTreeWidget extends UI.Widget.Widget {
2660
2714
  }
2661
2715
 
2662
2716
  Badges.UserBadges.instance().recordAction(Badges.BadgeAction.DOM_ELEMENT_OR_ATTRIBUTE_EDITED);
2717
+ const changeTracker = this.changeTracker;
2718
+ Elements.DOMChanges.trackTagNameEdit(changeTracker, newNode, buildChangeSelector(changeTracker, newNode),
2719
+ oldText ?? tagName ?? '', newText);
2663
2720
  if (this.selectNodeAfterEdit) {
2664
2721
  this.selectNodeAfterEdit(wasExpanded, error, newNode, moveDirection);
2665
2722
  }
@@ -2668,8 +2725,14 @@ export class ElementsTreeWidget extends UI.Widget.Widget {
2668
2725
 
2669
2726
  private textNodeEditingCommitted(textNode: SDK.DOMModel.DOMNode, _element: Element, newText: string): void {
2670
2727
  this.editing = null;
2728
+ const oldValue = textNode.nodeValue() ?? '';
2671
2729
 
2672
- function callback(this: ElementsTreeWidget): void {
2730
+ function callback(this: ElementsTreeWidget, error?: string|null): void {
2731
+ if (!error && oldValue !== newText) {
2732
+ const changeTracker = this.changeTracker;
2733
+ Elements.DOMChanges.trackTextNodeEdit(changeTracker, textNode, buildChangeSelector(changeTracker, textNode),
2734
+ oldValue, newText);
2735
+ }
2673
2736
  this.#clearDOMNextUpdate = true;
2674
2737
  this.updateTitle();
2675
2738
  }
@@ -2821,7 +2884,7 @@ export class ElementsTreeWidget extends UI.Widget.Widget {
2821
2884
  async remove(): Promise<void> {
2822
2885
  if (this.isToggledToHidden?.(this.node)) {
2823
2886
  // Unhide the node before removing. This avoids inconsistent state if the node is restored via undo.
2824
- await this.toggleHideElement?.(this.node);
2887
+ await this.node.toggleHideElement();
2825
2888
  }
2826
2889
  if (this.node.pseudoType()) {
2827
2890
  return;
@@ -2830,7 +2893,14 @@ export class ElementsTreeWidget extends UI.Widget.Widget {
2830
2893
  if (!this.node.parentNode || this.node.parentNode.nodeType() === Node.DOCUMENT_NODE) {
2831
2894
  return;
2832
2895
  }
2833
- void this.node.removeNode();
2896
+ // The selector has to be resolved before the node is detached from the tree.
2897
+ const changeTracker = this.changeTracker;
2898
+ const selector = buildChangeSelector(changeTracker, this.node);
2899
+ await this.node.removeNode((err: string|null) => {
2900
+ if (!err) {
2901
+ Elements.DOMChanges.trackNodeRemoval(changeTracker, this.node, selector);
2902
+ }
2903
+ });
2834
2904
  }
2835
2905
 
2836
2906
  toggleEditAsHTML(callback?: ((arg0: boolean) => void), startEditing?: boolean): void {
@@ -2851,7 +2921,15 @@ export class ElementsTreeWidget extends UI.Widget.Widget {
2851
2921
 
2852
2922
  const commitChange = (initialValue: string, value: string): void => {
2853
2923
  if (initialValue !== value) {
2854
- node.setOuterHTML(value, selectNode);
2924
+ // The selector has to be resolved before the node is detached from the tree.
2925
+ const changeTracker = this.changeTracker;
2926
+ const selector = buildChangeSelector(changeTracker, node);
2927
+ node.setOuterHTML(value, (error: string|null) => {
2928
+ if (!error) {
2929
+ Elements.DOMChanges.trackHTMLEdit(changeTracker, node, selector, initialValue, value);
2930
+ }
2931
+ selectNode(error);
2932
+ });
2855
2933
  }
2856
2934
  };
2857
2935