chrome-devtools-frontend 1.0.1642845 → 1.0.1642899

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 (51) hide show
  1. package/SECURITY.md +1 -0
  2. package/front_end/core/host/UserMetrics.ts +2 -1
  3. package/front_end/core/sdk/CSSMatchedStyles.ts +55 -26
  4. package/front_end/core/sdk/CSSRule.ts +1 -0
  5. package/front_end/core/sdk/DebuggerModel.ts +5 -0
  6. package/front_end/entrypoints/greendev_floaty/FloatyEntrypoint.ts +4 -3
  7. package/front_end/entrypoints/greendev_floaty/greendev_floaty.ts +4 -3
  8. package/front_end/models/ai_assistance/AiAgent2.ts +80 -16
  9. package/front_end/models/ai_assistance/AiConversation.ts +3 -2
  10. package/front_end/models/ai_assistance/README.md +8 -0
  11. package/front_end/models/ai_assistance/agents/AccessibilityAgent.ts +50 -35
  12. package/front_end/models/ai_assistance/agents/AiAgent.ts +16 -0
  13. package/front_end/models/ai_assistance/agents/ContextSelectionAgent.ts +2 -2
  14. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +195 -147
  15. package/front_end/models/ai_assistance/agents/StylingAgent.snapshot.txt +0 -25
  16. package/front_end/models/ai_assistance/agents/StylingAgent.ts +24 -305
  17. package/front_end/models/ai_assistance/ai_assistance.ts +8 -0
  18. package/front_end/models/ai_assistance/contexts/DOMNodeContext.snapshot.txt +51 -0
  19. package/front_end/models/ai_assistance/contexts/DOMNodeContext.ts +200 -0
  20. package/front_end/models/ai_assistance/skills/styling.md +36 -2
  21. package/front_end/models/ai_assistance/tools/GetStyles.ts +137 -0
  22. package/front_end/models/ai_assistance/tools/Tool.ts +55 -0
  23. package/front_end/models/ai_assistance/tools/ToolRegistry.ts +34 -0
  24. package/front_end/models/lighthouse/LighthouseReporterTypes.ts +5 -0
  25. package/front_end/models/live-metrics/LiveMetrics.ts +24 -13
  26. package/front_end/models/stack_trace/DetailedErrorStackParser.ts +2 -2
  27. package/front_end/models/stack_trace/StackTrace.ts +4 -1
  28. package/front_end/models/stack_trace/StackTraceImpl.ts +9 -2
  29. package/front_end/models/stack_trace/StackTraceModel.ts +17 -4
  30. package/front_end/models/stack_trace/Trie.ts +1 -1
  31. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +19 -15
  32. package/front_end/panels/ai_assistance/ai_assistance-meta.ts +16 -0
  33. package/front_end/panels/ai_assistance/components/ChatInput.ts +2 -2
  34. package/front_end/panels/application/DOMStorageItemsView.ts +4 -0
  35. package/front_end/panels/application/KeyValueStorageItemsView.ts +39 -7
  36. package/front_end/panels/common/ExtensionServer.ts +26 -15
  37. package/front_end/panels/elements/StandaloneStylesContainer.ts +1 -1
  38. package/front_end/panels/elements/StylePropertiesSection.ts +8 -0
  39. package/front_end/panels/elements/StylePropertyHighlighter.ts +4 -2
  40. package/front_end/panels/elements/StylePropertyTreeElement.ts +6 -5
  41. package/front_end/panels/elements/StylesContainer.ts +1 -1
  42. package/front_end/panels/elements/StylesSidebarPane.ts +4 -4
  43. package/front_end/panels/layer_viewer/PaintProfilerView.ts +106 -132
  44. package/front_end/panels/lighthouse/LighthousePanel.ts +4 -3
  45. package/front_end/panels/network/NetworkLogView.ts +3 -0
  46. package/front_end/panels/network/networkLogView.css +0 -15
  47. package/front_end/ui/legacy/components/cookie_table/CookiesTable.ts +36 -3
  48. package/front_end/ui/legacy/components/data_grid/dataGridAiButton.css +20 -0
  49. package/front_end/ui/legacy/components/utils/Linkifier.ts +19 -4
  50. package/front_end/ui/visual_logging/KnownContextValues.ts +1 -0
  51. package/package.json +1 -1
@@ -3,7 +3,6 @@
3
3
  // found in the LICENSE file.
4
4
 
5
5
  import * as Host from '../../../core/host/host.js';
6
- import * as i18n from '../../../core/i18n/i18n.js';
7
6
  import * as Root from '../../../core/root/root.js';
8
7
  import * as SDK from '../../../core/sdk/sdk.js';
9
8
  import * as Protocol from '../../../generated/protocol.js';
@@ -11,16 +10,15 @@ import * as Greendev from '../../../models/greendev/greendev.js';
11
10
  import * as Annotations from '../../annotations/annotations.js';
12
11
  import * as Emulation from '../../emulation/emulation.js';
13
12
  import {ChangeManager} from '../ChangeManager.js';
14
- import {debugLog} from '../debug.js';
15
13
  import {ExtensionScope} from '../ExtensionScope.js';
16
14
  import {AI_ASSISTANCE_CSS_CLASS_NAME} from '../injected.js';
15
+ import {ToolName} from '../tools/Tool.js';
16
+ import {ToolRegistry} from '../tools/ToolRegistry.js';
17
17
 
18
18
  import {
19
19
  AiAgent,
20
- type ComputedStyleAiWidget,
21
20
  type ContextResponse,
22
- ConversationContext,
23
- type ConversationSuggestions,
21
+ type ConversationContext,
24
22
  type FunctionCallHandlerResult,
25
23
  type MultimodalInput,
26
24
  MultimodalInputType,
@@ -35,18 +33,6 @@ import {
35
33
  JavascriptExecutor
36
34
  } from './ExecuteJavascript.js';
37
35
 
38
- /*
39
- * Strings that don't need to be translated at this time.
40
- */
41
- const UIStringsNotTranslate = {
42
- /**
43
- * @description Heading text for context details of Freestyler agent.
44
- */
45
- dataUsed: 'Data used',
46
- } as const;
47
-
48
- const lockedString = i18n.i18n.lockedString;
49
-
50
36
  const preamble = `You are the most advanced CSS/DOM/HTML debugging assistant integrated into Chrome DevTools.
51
37
  You always suggest considering the best web development practices and the newest platform features such as view transitions.
52
38
  The user selected a DOM element in the browser's DevTools and sends a query about the page or the selected DOM element.
@@ -152,78 +138,6 @@ const MULTIMODAL_ENHANCEMENT_PROMPTS: Record<MultimodalInputType, string> = {
152
138
 
153
139
  export const AI_ASSISTANCE_FILTER_REGEX = `\\.${AI_ASSISTANCE_CSS_CLASS_NAME}-.*&`;
154
140
 
155
- export class NodeContext extends ConversationContext<SDK.DOMModel.DOMNode> {
156
- #node: SDK.DOMModel.DOMNode;
157
-
158
- constructor(node: SDK.DOMModel.DOMNode) {
159
- super();
160
- this.#node = node;
161
- }
162
-
163
- override getURL(): string {
164
- const ownerDocument = this.#node.ownerDocument;
165
- if (!ownerDocument) {
166
- // The node is detached from a document.
167
- return 'detached';
168
- }
169
- return ownerDocument.documentURL;
170
- }
171
-
172
- getItem(): SDK.DOMModel.DOMNode {
173
- return this.#node;
174
- }
175
-
176
- override getTitle(): string {
177
- throw new Error('Not implemented');
178
- }
179
-
180
- override async getSuggestions(): Promise<ConversationSuggestions|undefined> {
181
- const layoutProps = await this.#node.domModel().cssModel().getLayoutPropertiesFromComputedStyle(this.#node.id);
182
-
183
- if (!layoutProps) {
184
- return;
185
- }
186
-
187
- if (layoutProps.isFlex) {
188
- return [
189
- {title: 'How can I make flex items wrap?', jslogContext: 'flex-wrap'},
190
- {title: 'How do I distribute flex items evenly?', jslogContext: 'flex-distribute'},
191
- {title: 'What is flexbox?', jslogContext: 'flex-what'},
192
- ];
193
- }
194
- if (layoutProps.isSubgrid) {
195
- return [
196
- {title: 'Where is this grid defined?', jslogContext: 'subgrid-where'},
197
- {title: 'How to overwrite parent grid properties?', jslogContext: 'subgrid-override'},
198
- {title: 'How do subgrids work? ', jslogContext: 'subgrid-how'},
199
- ];
200
- }
201
- if (layoutProps.isGrid) {
202
- return [
203
- {title: 'How do I align items in a grid?', jslogContext: 'grid-align'},
204
- {title: 'How to add spacing between grid items?', jslogContext: 'grid-gap'},
205
- {title: 'How does grid layout work?', jslogContext: 'grid-how'},
206
- ];
207
- }
208
- if (layoutProps.hasScroll) {
209
- return [
210
- {title: 'How do I remove scrollbars for this element?', jslogContext: 'scroll-remove'},
211
- {title: 'How can I style a scrollbar?', jslogContext: 'scroll-style'},
212
- {title: 'Why does this element scroll?', jslogContext: 'scroll-why'},
213
- ];
214
- }
215
- if (layoutProps.containerType) {
216
- return [
217
- {title: 'What are container queries?', jslogContext: 'container-what'},
218
- {title: 'How do I use container-type?', jslogContext: 'container-how'},
219
- {title: 'What\'s the container context for this element?', jslogContext: 'container-context'},
220
- ];
221
- }
222
-
223
- return;
224
- }
225
- }
226
-
227
141
  /**
228
142
  * One agent instance handles one conversation. Create a new agent
229
143
  * instance for a new conversation.
@@ -282,57 +196,17 @@ export class StylingAgent extends AiAgent<SDK.DOMModel.DOMNode> {
282
196
  },
283
197
  this.#execJs);
284
198
 
285
- this.declareFunction<{
286
- elements: number[],
287
- styleProperties: string[],
288
- explanation: string,
289
- }>('getStyles', {
290
- description:
291
- `Get computed and source styles for one or multiple elements on the inspected page for multiple elements at once by uid.
292
-
293
- **CRITICAL** An element uid is a number, not a selector.
294
- **CRITICAL** Use selectors to refer to elements in the text output. Do not use uids.
295
- **CRITICAL** Always provide the explanation argument to explain what and why you query.
296
- **CRITICAL** You MUST provide a specific list of CSS property names. Do not use generic values like "all" or "*".`,
297
- parameters: {
298
- type: Host.AidaClient.ParametersTypes.OBJECT,
299
- description: '',
300
- nullable: false,
301
- properties: {
302
- explanation: {
303
- type: Host.AidaClient.ParametersTypes.STRING,
304
- description: 'Explain why you want to get styles',
305
- nullable: false,
306
- },
307
- elements: {
308
- type: Host.AidaClient.ParametersTypes.ARRAY,
309
- description: 'A list of element uids to get data for. These are numbers, not selectors.',
310
- items: {type: Host.AidaClient.ParametersTypes.INTEGER, description: `An element uid.`},
311
- nullable: false,
312
- },
313
- styleProperties: {
314
- type: Host.AidaClient.ParametersTypes.ARRAY,
315
- description:
316
- 'One or more specific CSS style property names to fetch. Generic values like "all" or "*" are not supported.',
317
- nullable: false,
318
- items: {
319
- type: Host.AidaClient.ParametersTypes.STRING,
320
- description: 'A CSS style property name to retrieve. For example, \'background-color\'.'
321
- }
322
- },
323
- },
324
- required: ['explanation', 'elements', 'styleProperties']
325
- },
326
- displayInfoFromArgs: params => {
327
- return {
328
- title: 'Reading computed and source styles',
329
- thought: params.explanation,
330
- action: `getStyles(${JSON.stringify(params.elements)}, ${JSON.stringify(params.styleProperties)})`,
331
- };
332
- },
333
- handler: async params => {
334
- return await this.#getStyles(params.elements, params.styleProperties);
335
- },
199
+ const getStylesTool = ToolRegistry.get(ToolName.GET_STYLES);
200
+ if (!getStylesTool) {
201
+ throw new Error('Required tool "getStyles" not found');
202
+ }
203
+ this.declareFunction(ToolName.GET_STYLES, {
204
+ description: getStylesTool.description,
205
+ parameters: getStylesTool.parameters,
206
+ displayInfoFromArgs: getStylesTool.displayInfoFromArgs,
207
+ handler: args => getStylesTool.handler(args, {
208
+ conversationContext: this.context ?? null,
209
+ }),
336
210
  });
337
211
 
338
212
  this.declareFunction('executeJavaScript', executeJavaScriptFunction(this.#javascriptExecutor));
@@ -401,162 +275,10 @@ export class StylingAgent extends AiAgent<SDK.DOMModel.DOMNode> {
401
275
  });
402
276
  }
403
277
 
404
- static async describeElement(element: SDK.DOMModel.DOMNode): Promise<string> {
405
- let output = `* Element's uid is ${element.backendNodeId()}.
406
- * Its selector is \`${element.simpleSelector()}\``;
407
- const childNodes = await element.getChildNodesPromise();
408
- if (childNodes) {
409
- const textChildNodes = childNodes.filter(childNode => childNode.nodeType() === Node.TEXT_NODE);
410
- const elementChildNodes = childNodes.filter(childNode => childNode.nodeType() === Node.ELEMENT_NODE);
411
- switch (elementChildNodes.length) {
412
- case 0:
413
- output += '\n* It doesn\'t have any child element nodes';
414
- break;
415
- case 1:
416
- output += `\n* It only has 1 child element node: \`${elementChildNodes[0].simpleSelector()}\``;
417
- break;
418
- default:
419
- output += `\n* It has ${elementChildNodes.length} child element nodes: ${
420
- elementChildNodes.map(node => `\`${node.simpleSelector()}\` (uid=${node.backendNodeId()})`).join(', ')}`;
421
- }
422
-
423
- switch (textChildNodes.length) {
424
- case 0:
425
- output += '\n* It doesn\'t have any child text nodes';
426
- break;
427
- case 1:
428
- output += '\n* It only has 1 child text node';
429
- break;
430
- default:
431
- output += `\n* It has ${textChildNodes.length} child text nodes`;
432
- }
433
- }
434
-
435
- if (element.nextSibling) {
436
- const elementOrNodeElementNodeText = element.nextSibling.nodeType() === Node.ELEMENT_NODE ?
437
- `an element (uid=${element.nextSibling.backendNodeId()})` :
438
- 'a non element';
439
- output += `\n* It has a next sibling and it is ${elementOrNodeElementNodeText} node`;
440
- }
441
-
442
- if (element.previousSibling) {
443
- const elementOrNodeElementNodeText = element.previousSibling.nodeType() === Node.ELEMENT_NODE ?
444
- `an element (uid=${element.previousSibling.backendNodeId()})` :
445
- 'a non element';
446
- output += `\n* It has a previous sibling and it is ${elementOrNodeElementNodeText} node`;
447
- }
448
-
449
- if (element.isInShadowTree()) {
450
- output += '\n* It is in a shadow DOM tree.';
451
- }
452
-
453
- const parentNode = element.parentNode;
454
- if (parentNode) {
455
- const parentChildrenNodes = await parentNode.getChildNodesPromise();
456
- output += `\n* Its parent's selector is \`${parentNode.simpleSelector()}\` (uid=${parentNode.backendNodeId()})`;
457
- const elementOrNodeElementNodeText = parentNode.nodeType() === Node.ELEMENT_NODE ? 'an element' : 'a non element';
458
- output += `\n* Its parent is ${elementOrNodeElementNodeText} node`;
459
- if (parentNode.isShadowRoot()) {
460
- output += '\n* Its parent is a shadow root.';
461
- }
462
- if (parentChildrenNodes) {
463
- const childElementNodes =
464
- parentChildrenNodes.filter(siblingNode => siblingNode.nodeType() === Node.ELEMENT_NODE);
465
- switch (childElementNodes.length) {
466
- case 0:
467
- break;
468
- case 1:
469
- output += '\n* Its parent has only 1 child element node';
470
- break;
471
- default:
472
- output += `\n* Its parent has ${childElementNodes.length} child element nodes: ${
473
- childElementNodes.map(node => `\`${node.simpleSelector()}\` (uid=${node.backendNodeId()})`)
474
- .join(', ')}`;
475
- break;
476
- }
477
-
478
- const siblingTextNodes = parentChildrenNodes.filter(siblingNode => siblingNode.nodeType() === Node.TEXT_NODE);
479
- switch (siblingTextNodes.length) {
480
- case 0:
481
- break;
482
- case 1:
483
- output += '\n* Its parent has only 1 child text node';
484
- break;
485
- default:
486
- output += `\n* Its parent has ${siblingTextNodes.length} child text nodes: ${
487
- siblingTextNodes.map(node => `\`${node.simpleSelector()}\``).join(', ')}`;
488
- break;
489
- }
490
- }
491
- }
492
-
493
- return output.trim();
494
- }
495
-
496
278
  #getSelectedNode(): SDK.DOMModel.DOMNode|null {
497
279
  return this.context?.getItem() ?? null;
498
280
  }
499
281
 
500
- async #getStyles(elements: number[], properties: string[]): Promise<FunctionCallHandlerResult<unknown>> {
501
- const widgets: ComputedStyleAiWidget[] = [];
502
-
503
- const result:
504
- Record<string, {computed: Record<string, string|undefined>, authored: Record<string, string|undefined>}> = {};
505
- for (const uid of elements) {
506
- result[uid] = {computed: {}, authored: {}};
507
- debugLog(`Action to execute: uid=${uid}`);
508
- const selectedNode = this.#getSelectedNode();
509
- if (!selectedNode) {
510
- return {error: 'Error: Could not find the currently selected element.'};
511
- }
512
- const node = new SDK.DOMModel.DeferredDOMNode(
513
- selectedNode.domModel().target(), Number(uid) as unknown as Protocol.DOM.BackendNodeId);
514
- const resolved = await node.resolvePromise();
515
- if (!resolved) {
516
- return {error: 'Error: Could not find the element with uid=' + uid};
517
- }
518
- const newContext = new NodeContext(resolved);
519
- if (this.context?.getOrigin() !== newContext.getOrigin()) {
520
- return {error: 'Error: Node does not belong to the current origin.'};
521
- }
522
- const styles = await resolved.domModel().cssModel().getComputedStyle(resolved.id);
523
- if (!styles) {
524
- return {error: 'Error: Could not get computed styles.'};
525
- }
526
- const matchedStyles = await resolved.domModel().cssModel().getMatchedStyles(resolved.id);
527
- if (!matchedStyles) {
528
- return {error: 'Error: Could not get authored styles.'};
529
- }
530
- widgets.push({
531
- name: 'COMPUTED_STYLES',
532
- data: {
533
- computedStyles: styles,
534
- backendNodeId: node.backendNodeId(),
535
- matchedCascade: matchedStyles,
536
- properties,
537
- }
538
- });
539
- for (const prop of properties) {
540
- result[uid].computed[prop] = styles.get(prop);
541
- }
542
- for (const style of matchedStyles.nodeStyles()) {
543
- for (const property of style.allProperties()) {
544
- if (!properties.includes(property.name)) {
545
- continue;
546
- }
547
- const state = matchedStyles.propertyState(property);
548
- if (state === SDK.CSSMatchedStyles.PropertyState.ACTIVE) {
549
- result[uid].authored[property.name] = property.value;
550
- }
551
- }
552
- }
553
- }
554
- return {
555
- result: JSON.stringify(result, null, 2),
556
- widgets,
557
- };
558
- }
559
-
560
282
  async addElementAnnotation(elementId: string, annotationMessage: string):
561
283
  Promise<FunctionCallHandlerResult<unknown>> {
562
284
  if (!Annotations.AnnotationRepository.annotationsEnabled()) {
@@ -782,16 +504,15 @@ export class StylingAgent extends AiAgent<SDK.DOMModel.DOMNode> {
782
504
  override async *
783
505
  handleContextDetails(selectedElement: ConversationContext<SDK.DOMModel.DOMNode>|null):
784
506
  AsyncGenerator<ContextResponse, void, void> {
785
- if (!selectedElement) {
786
- return;
507
+ if (selectedElement) {
508
+ const details = await selectedElement.getUserFacingDetails();
509
+ if (details) {
510
+ yield {
511
+ type: ResponseType.CONTEXT,
512
+ details,
513
+ };
514
+ }
787
515
  }
788
- yield {
789
- type: ResponseType.CONTEXT,
790
- details: [{
791
- title: lockedString(UIStringsNotTranslate.dataUsed),
792
- text: await StylingAgent.describeElement(selectedElement.getItem()),
793
- }],
794
- };
795
516
  }
796
517
 
797
518
  protected override async preRun(): Promise<void> {
@@ -814,10 +535,8 @@ export class StylingAgent extends AiAgent<SDK.DOMModel.DOMNode> {
814
535
  this.#hasAddedEmulationInstructions = true;
815
536
  }
816
537
 
817
- const elementEnchancementQuery = selectedElement ?
818
- `# Inspected element\n\n${
819
- await StylingAgent.describeElement(selectedElement.getItem())}\n\n# User request\n\n` :
820
- '';
538
+ const promptDetails = selectedElement ? await selectedElement.getPromptDetails() : null;
539
+ const elementEnchancementQuery = promptDetails ? `${promptDetails}\n\n# User request\n\n` : '';
821
540
  return `${multimodalInputEnhancementQuery}${elementEnchancementQuery}QUERY: ${query}`;
822
541
  }
823
542
  }
@@ -24,6 +24,7 @@ import * as AiOrigins from './AiOrigins.js';
24
24
  import * as AiUtils from './AiUtils.js';
25
25
  import * as BuiltInAi from './BuiltInAi.js';
26
26
  import * as ChangeManager from './ChangeManager.js';
27
+ import * as DOMNodeContext from './contexts/DOMNodeContext.js';
27
28
  import * as FileFormatter from './data_formatters/FileFormatter.js';
28
29
  import * as LighthouseFormatter from './data_formatters/LighthouseFormatter.js';
29
30
  import * as NetworkRequestFormatter from './data_formatters/NetworkRequestFormatter.js';
@@ -38,6 +39,9 @@ import * as AICallTree from './performance/AICallTree.js';
38
39
  import * as AIContext from './performance/AIContext.js';
39
40
  import * as AIQueries from './performance/AIQueries.js';
40
41
  import * as StorageItem from './StorageItem.js';
42
+ import * as GetStyles from './tools/GetStyles.js';
43
+ import * as Tool from './tools/Tool.js';
44
+ import * as ToolRegistry from './tools/ToolRegistry.js';
41
45
 
42
46
  export {
43
47
  AccessibilityAgent,
@@ -56,10 +60,12 @@ export {
56
60
  ContextSelectionAgent,
57
61
  ConversationSummaryAgent,
58
62
  Debug,
63
+ DOMNodeContext,
59
64
  EvaluateAction,
60
65
  ExtensionScope,
61
66
  FileAgent,
62
67
  FileFormatter,
68
+ GetStyles,
63
69
  GreenDevAgent,
64
70
  GreenDevAgentAntigravityCliSocketClient,
65
71
  GreenDevAgentGeminiCliSocketClient,
@@ -75,5 +81,7 @@ export {
75
81
  StorageAgent,
76
82
  StorageItem,
77
83
  StylingAgent,
84
+ Tool,
85
+ ToolRegistry,
78
86
  UnitFormatters,
79
87
  };
@@ -0,0 +1,51 @@
1
+ Title: DOMNodeContext getPromptDetails describes the node correctly
2
+ Content:
3
+ # Inspected element
4
+
5
+ * Element's uid is 99.
6
+ * Its selector is `div#myElement`
7
+ === end content
8
+
9
+ Title: DOMNodeContext getUserFacingDetails returns details with Data Used title
10
+ Content:
11
+ [
12
+ {
13
+ "title": "Data used",
14
+ "text": "* Element's uid is 99.\n* Its selector is `div#myElement`"
15
+ }
16
+ ]
17
+ === end content
18
+
19
+ Title: DOMNodeContext describes an element with child nodes not loaded
20
+ Content:
21
+ * Element's uid is 99.
22
+ * Its selector is `div#myElement`
23
+ === end content
24
+
25
+ Title: DOMNodeContext describes an element with no children, siblings, or parent
26
+ Content:
27
+ * Element's uid is 99.
28
+ * Its selector is `div#myElement`
29
+ * It doesn't have any child element nodes
30
+ * It doesn't have any child text nodes
31
+ === end content
32
+
33
+ Title: DOMNodeContext describes an element with child element and text nodes
34
+ Content:
35
+ * Element's uid is 99.
36
+ * Its selector is `div#parentElement`
37
+ * It has 2 child element nodes: `span.child1` (uid=undefined), `span.child2` (uid=undefined)
38
+ * It only has 1 child text node
39
+ === end content
40
+
41
+ Title: DOMNodeContext describes an element with siblings and a parent
42
+ Content:
43
+ * Element's uid is 99.
44
+ * Its selector is `div#parentElement`
45
+ * It has a next sibling and it is an element (uid=undefined) node
46
+ * It has a previous sibling and it is a non element node
47
+ * Its parent's selector is `div#grandparentElement` (uid=undefined)
48
+ * Its parent is a non element node
49
+ * Its parent has only 1 child element node
50
+ * Its parent has only 1 child text node
51
+ === end content
@@ -0,0 +1,200 @@
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 * as i18n from '../../../core/i18n/i18n.js';
6
+ import type * as SDK from '../../../core/sdk/sdk.js';
7
+ import {
8
+ type ContextDetail,
9
+ ConversationContext,
10
+ type ConversationSuggestions,
11
+ } from '../agents/AiAgent.js';
12
+
13
+ const UIStringsNotTranslate = {
14
+ /**
15
+ * @description Heading text for context details of DevTools AI Agent.
16
+ */
17
+ dataUsed: 'Data used',
18
+ } as const;
19
+
20
+ const lockedString = i18n.i18n.lockedString;
21
+
22
+ export class DOMNodeContext extends ConversationContext<SDK.DOMModel.DOMNode> {
23
+ #node: SDK.DOMModel.DOMNode;
24
+
25
+ constructor(node: SDK.DOMModel.DOMNode) {
26
+ super();
27
+ this.#node = node;
28
+ }
29
+
30
+ override getURL(): string {
31
+ const ownerDocument = this.#node.ownerDocument;
32
+ if (!ownerDocument) {
33
+ // The node is detached from a document.
34
+ return 'detached';
35
+ }
36
+ return ownerDocument.documentURL;
37
+ }
38
+
39
+ getItem(): SDK.DOMModel.DOMNode {
40
+ return this.#node;
41
+ }
42
+
43
+ override getTitle(): string {
44
+ throw new Error('Not implemented');
45
+ }
46
+
47
+ override async getSuggestions(): Promise<ConversationSuggestions|undefined> {
48
+ const layoutProps = await this.#node.domModel().cssModel().getLayoutPropertiesFromComputedStyle(this.#node.id);
49
+
50
+ if (!layoutProps) {
51
+ return;
52
+ }
53
+
54
+ if (layoutProps.isFlex) {
55
+ return [
56
+ {title: 'How can I make flex items wrap?', jslogContext: 'flex-wrap'},
57
+ {title: 'How do I distribute flex items evenly?', jslogContext: 'flex-distribute'},
58
+ {title: 'What is flexbox?', jslogContext: 'flex-what'},
59
+ ];
60
+ }
61
+ if (layoutProps.isSubgrid) {
62
+ return [
63
+ {title: 'Where is this grid defined?', jslogContext: 'subgrid-where'},
64
+ {title: 'How to overwrite parent grid properties?', jslogContext: 'subgrid-override'},
65
+ {title: 'How do subgrids work? ', jslogContext: 'subgrid-how'},
66
+ ];
67
+ }
68
+ if (layoutProps.isGrid) {
69
+ return [
70
+ {title: 'How do I align items in a grid?', jslogContext: 'grid-align'},
71
+ {title: 'How to add spacing between grid items?', jslogContext: 'grid-gap'},
72
+ {title: 'How does grid layout work?', jslogContext: 'grid-how'},
73
+ ];
74
+ }
75
+ if (layoutProps.hasScroll) {
76
+ return [
77
+ {title: 'How do I remove scrollbars for this element?', jslogContext: 'scroll-remove'},
78
+ {title: 'How can I style a scrollbar?', jslogContext: 'scroll-style'},
79
+ {title: 'Why does this element scroll?', jslogContext: 'scroll-why'},
80
+ ];
81
+ }
82
+ if (layoutProps.containerType) {
83
+ return [
84
+ {title: 'What are container queries?', jslogContext: 'container-what'},
85
+ {title: 'How do I use container-type?', jslogContext: 'container-how'},
86
+ {title: 'What\'s the container context for this element?', jslogContext: 'container-context'},
87
+ ];
88
+ }
89
+
90
+ return;
91
+ }
92
+
93
+ override async getPromptDetails(): Promise<string|null> {
94
+ return `# Inspected element
95
+
96
+ ${await this.describe()}`;
97
+ }
98
+
99
+ override async getUserFacingDetails(): Promise<[ContextDetail, ...ContextDetail[]]|null> {
100
+ return [
101
+ {
102
+ title: lockedString(UIStringsNotTranslate.dataUsed),
103
+ text: await this.describe(),
104
+ },
105
+ ];
106
+ }
107
+
108
+ async describe(): Promise<string> {
109
+ const element = this.#node;
110
+ let output = `* Element's uid is ${element.backendNodeId()}.
111
+ * Its selector is \`${element.simpleSelector()}\``;
112
+ const childNodes = await element.getChildNodesPromise();
113
+ if (childNodes) {
114
+ const textChildNodes = childNodes.filter(childNode => childNode.nodeType() === Node.TEXT_NODE);
115
+ const elementChildNodes = childNodes.filter(childNode => childNode.nodeType() === Node.ELEMENT_NODE);
116
+ switch (elementChildNodes.length) {
117
+ case 0:
118
+ output += '\n* It doesn\'t have any child element nodes';
119
+ break;
120
+ case 1:
121
+ output += `\n* It only has 1 child element node: \`${elementChildNodes[0].simpleSelector()}\``;
122
+ break;
123
+ default:
124
+ output += `\n* It has ${elementChildNodes.length} child element nodes: ${
125
+ elementChildNodes.map(node => `\`${node.simpleSelector()}\` (uid=${node.backendNodeId()})`).join(', ')}`;
126
+ }
127
+
128
+ switch (textChildNodes.length) {
129
+ case 0:
130
+ output += '\n* It doesn\'t have any child text nodes';
131
+ break;
132
+ case 1:
133
+ output += '\n* It only has 1 child text node';
134
+ break;
135
+ default:
136
+ output += `\n* It has ${textChildNodes.length} child text nodes`;
137
+ }
138
+ }
139
+
140
+ if (element.nextSibling) {
141
+ const elementOrNodeElementNodeText = element.nextSibling.nodeType() === Node.ELEMENT_NODE ?
142
+ `an element (uid=${element.nextSibling.backendNodeId()})` :
143
+ 'a non element';
144
+ output += `\n* It has a next sibling and it is ${elementOrNodeElementNodeText} node`;
145
+ }
146
+
147
+ if (element.previousSibling) {
148
+ const elementOrNodeElementNodeText = element.previousSibling.nodeType() === Node.ELEMENT_NODE ?
149
+ `an element (uid=${element.previousSibling.backendNodeId()})` :
150
+ 'a non element';
151
+ output += `\n* It has a previous sibling and it is ${elementOrNodeElementNodeText} node`;
152
+ }
153
+
154
+ if (element.isInShadowTree()) {
155
+ output += '\n* It is in a shadow DOM tree.';
156
+ }
157
+
158
+ const parentNode = element.parentNode;
159
+ if (parentNode) {
160
+ const parentChildrenNodes = await parentNode.getChildNodesPromise();
161
+ output += `\n* Its parent's selector is \`${parentNode.simpleSelector()}\` (uid=${parentNode.backendNodeId()})`;
162
+ const elementOrNodeElementNodeText = parentNode.nodeType() === Node.ELEMENT_NODE ? 'an element' : 'a non element';
163
+ output += `\n* Its parent is ${elementOrNodeElementNodeText} node`;
164
+ if (parentNode.isShadowRoot()) {
165
+ output += '\n* Its parent is a shadow root.';
166
+ }
167
+ if (parentChildrenNodes) {
168
+ const childElementNodes =
169
+ parentChildrenNodes.filter(siblingNode => siblingNode.nodeType() === Node.ELEMENT_NODE);
170
+ switch (childElementNodes.length) {
171
+ case 0:
172
+ break;
173
+ case 1:
174
+ output += '\n* Its parent has only 1 child element node';
175
+ break;
176
+ default:
177
+ output += `\n* Its parent has ${childElementNodes.length} child element nodes: ${
178
+ childElementNodes.map(node => `\`${node.simpleSelector()}\` (uid=${node.backendNodeId()})`)
179
+ .join(', ')}`;
180
+ break;
181
+ }
182
+
183
+ const siblingTextNodes = parentChildrenNodes.filter(siblingNode => siblingNode.nodeType() === Node.TEXT_NODE);
184
+ switch (siblingTextNodes.length) {
185
+ case 0:
186
+ break;
187
+ case 1:
188
+ output += '\n* Its parent has only 1 child text node';
189
+ break;
190
+ default:
191
+ output += `\n* Its parent has ${siblingTextNodes.length} child text nodes: ${
192
+ siblingTextNodes.map(node => `\`${node.simpleSelector()}\``).join(', ')}`;
193
+ break;
194
+ }
195
+ }
196
+ }
197
+
198
+ return output.trim();
199
+ }
200
+ }