chrome-devtools-frontend 1.0.1709383 → 1.0.1711155

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 (82) hide show
  1. package/AUTHORS +1 -0
  2. package/front_end/core/common/ParsedURL.ts +20 -19
  3. package/front_end/core/sdk/DOMModel.ts +6 -4
  4. package/front_end/core/sdk/PageFunctions.ts +1 -1
  5. package/front_end/generated/InspectorBackendCommands.ts +1 -0
  6. package/front_end/generated/protocol-mapping.d.ts +7 -0
  7. package/front_end/generated/protocol-proxy-api.d.ts +5 -0
  8. package/front_end/generated/protocol.ts +12 -0
  9. package/front_end/models/ai_assistance/agents/AiAgent.ts +7 -4
  10. package/front_end/models/ai_assistance/agents/StylingAgent.ts +1 -0
  11. package/front_end/models/ai_assistance/contexts/LighthouseContext.snapshot.txt +6 -1
  12. package/front_end/models/ai_assistance/contexts/LighthouseContext.ts +3 -2
  13. package/front_end/models/ai_assistance/data_formatters/LighthouseFormatter.snapshot.txt +11 -0
  14. package/front_end/models/ai_assistance/data_formatters/LighthouseFormatter.ts +47 -19
  15. package/front_end/models/ai_assistance/skills/accessibility.md +1 -0
  16. package/front_end/models/ai_assistance/skills/lighthouse.md +1 -0
  17. package/front_end/models/ai_assistance/skills/styling.md +1 -0
  18. package/front_end/models/ai_assistance/tools/ListSources.ts +11 -5
  19. package/front_end/models/bindings/DebuggerWorkspaceBinding.ts +5 -1
  20. package/front_end/models/bindings/SourceMapStepping.ts +76 -4
  21. package/front_end/models/bindings/StylesSourceMapping.ts +3 -0
  22. package/front_end/models/emulation/DeviceModeModel.ts +9 -4
  23. package/front_end/models/har/HARFormat.ts +2 -7
  24. package/front_end/models/javascript_metadata/NativeFunctions.ts +52 -48
  25. package/front_end/models/persistence/Automapping.ts +8 -3
  26. package/front_end/models/trace/lantern/graph/PageDependencyGraph.ts +1 -1
  27. package/front_end/models/trace/lantern/metrics/TotalBlockingTime.ts +1 -1
  28. package/front_end/panels/application/BounceTrackingMitigationsTreeElement.ts +1 -1
  29. package/front_end/panels/application/CookieItemsView.ts +10 -10
  30. package/front_end/panels/application/DOMStorageItemsView.ts +7 -8
  31. package/front_end/panels/application/ExtensionStorageItemsView.ts +4 -5
  32. package/front_end/panels/application/IndexedDBViews.ts +29 -31
  33. package/front_end/panels/application/KeyValueStorageItemsView.ts +5 -5
  34. package/front_end/panels/application/ServiceWorkerCacheTreeElement.ts +6 -6
  35. package/front_end/panels/application/ServiceWorkerCacheViews.ts +17 -17
  36. package/front_end/panels/application/StorageBucketsTreeElement.ts +3 -10
  37. package/front_end/panels/application/StorageItemsToolbar.ts +6 -6
  38. package/front_end/panels/application/StorageView.ts +36 -34
  39. package/front_end/panels/application/TrustTokensTreeElement.ts +1 -2
  40. package/front_end/panels/common/CommentThreadWidget.ts +0 -17
  41. package/front_end/panels/common/CommentsOverlayWidget.ts +0 -4
  42. package/front_end/panels/common/ExtensionServer.ts +52 -50
  43. package/front_end/panels/common/commentThreadWidget.css +0 -4
  44. package/front_end/panels/elements/DOMPath.ts +7 -1
  45. package/front_end/panels/elements/DOMTreeWidget.ts +7 -3
  46. package/front_end/panels/elements/StylePropertyTreeElement.ts +1 -1
  47. package/front_end/panels/elements/StylePropertyUtils.ts +11 -3
  48. package/front_end/panels/emulation/DeviceModeView.ts +1 -1
  49. package/front_end/panels/layer_viewer/Layers3DView.ts +7 -8
  50. package/front_end/panels/layer_viewer/layer_viewer-meta.ts +1 -5
  51. package/front_end/panels/network/NetworkDataGridNode.ts +3 -0
  52. package/front_end/panels/network/NetworkLogView.ts +8 -13
  53. package/front_end/panels/security/SecurityPanel.ts +27 -20
  54. package/front_end/panels/sources/DebuggerPlugin.ts +2 -2
  55. package/front_end/panels/sources/SourcesPanel.ts +9 -1
  56. package/front_end/panels/sources/UISourceCodeFrame.ts +69 -64
  57. package/front_end/panels/timeline/CompatibilityTracksAppender.ts +4 -1
  58. package/front_end/panels/timeline/CountersGraph.ts +19 -10
  59. package/front_end/panels/timeline/ModificationsManager.ts +3 -1
  60. package/front_end/panels/timeline/ThreadAppender.ts +4 -1
  61. package/front_end/panels/timeline/TimelineController.ts +9 -2
  62. package/front_end/panels/timeline/TimelineDetailsView.ts +4 -1
  63. package/front_end/panels/timeline/TimelineEventOverview.ts +3 -5
  64. package/front_end/panels/timeline/TimelineFlameChartView.ts +1 -4
  65. package/front_end/panels/timeline/TimelinePanel.ts +27 -16
  66. package/front_end/panels/timeline/TimelineTreeView.ts +0 -6
  67. package/front_end/panels/timeline/TimelineUIUtils.ts +5 -3
  68. package/front_end/panels/timeline/components/SidebarAnnotationsTab.ts +3 -1
  69. package/front_end/panels/timeline/overlays/OverlaysImpl.ts +3 -1
  70. package/front_end/panels/timeline/overlays/components/TimeRangeOverlay.ts +147 -97
  71. package/front_end/panels/timeline/timeline-meta.ts +22 -84
  72. package/front_end/third_party/chromium/README.chromium +1 -1
  73. package/front_end/ui/components/text_editor/TextEditor.ts +18 -6
  74. package/front_end/ui/legacy/TabbedPane.ts +17 -1
  75. package/front_end/ui/legacy/TextPrompt.ts +80 -19
  76. package/front_end/ui/legacy/components/source_frame/ImageView.ts +11 -4
  77. package/front_end/ui/legacy/components/utils/Linkifier.ts +29 -17
  78. package/front_end/ui/settings/LayerViewerSettings.ts +14 -0
  79. package/front_end/ui/settings/TimelineSettings.ts +103 -0
  80. package/front_end/ui/settings/settings.ts +2 -0
  81. package/front_end/ui/visual_logging/KnownContextValues.ts +1 -0
  82. package/package.json +1 -1
package/AUTHORS CHANGED
@@ -60,6 +60,7 @@ Jan Keitel <jan.keitel@gmail.com>
60
60
  Jeffrey Chen <jeffreyca16@gmail.com>
61
61
  Jerome Vouillon <jerome.vouillon@gmail.com>
62
62
  Jesper van den Ende <jespertheend@gmail.com>
63
+ Jihyeon Lee <wlgus7464@gmail.com>
63
64
  Jithil p Ponnan <jithil.p@gmail.com>
64
65
  Joseph P <thepigrammer@gmail.com>
65
66
  Juba Borgohain <chromiumjuba@gmail.com>
@@ -57,26 +57,34 @@ const WEB_SAFE_SCHEMES = new Set([
57
57
  'data:',
58
58
  ]);
59
59
 
60
- /**
61
- * Returns true if the URL uses an unprivileged web-safe scheme (or is a `blob:` URL wrapping one).
62
- */
63
- export function hasWebSafeScheme(url: Platform.DevToolsPath.UrlString|URL|string): boolean {
60
+ function parseUnwrappedURL(url: Platform.DevToolsPath.UrlString|URL|string): URL|null {
64
61
  try {
65
- const parsed = new URL(url);
66
- if (parsed.protocol === 'blob:') {
67
- return hasWebSafeScheme(parsed.pathname);
62
+ let parsed = new URL(url);
63
+ // Iteratively unwrap nested `blob:` and `filesystem:` URLs (e.g. `blob:filesystem:https://...`)
64
+ // to inspect the underlying origin's protocol.
65
+ while (parsed.protocol === 'blob:' || parsed.protocol === 'filesystem:') {
66
+ parsed = new URL(parsed.href.slice(parsed.protocol.length));
68
67
  }
69
- return WEB_SAFE_SCHEMES.has(parsed.protocol);
68
+ return parsed;
70
69
  } catch {
71
- return false;
70
+ return null;
72
71
  }
73
72
  }
74
73
 
74
+ /**
75
+ * Returns true if the URL uses an unprivileged web-safe scheme (or is a `blob:`/`filesystem:` URL wrapping one).
76
+ */
77
+ export function hasWebSafeScheme(url: Platform.DevToolsPath.UrlString|URL|string): boolean {
78
+ const parsed = parseUnwrappedURL(url);
79
+ return parsed !== null && WEB_SAFE_SCHEMES.has(parsed.protocol);
80
+ }
81
+
75
82
  /**
76
83
  * Privileged browser and local schemes that must not be opened via external/new-tab links by default.
77
84
  */
78
85
  const PRIVILEGED_SCHEMES = new Set([
79
86
  'chrome:',
87
+ 'chrome-error:',
80
88
  'chrome-extension:',
81
89
  'chrome-search:',
82
90
  'chrome-untrusted:',
@@ -86,18 +94,11 @@ const PRIVILEGED_SCHEMES = new Set([
86
94
  ]);
87
95
 
88
96
  /**
89
- * Returns true if the URL uses a privileged scheme (or is a `blob:` URL wrapping one).
97
+ * Returns true if the URL uses a privileged scheme (or is a `blob:`/`filesystem:` URL wrapping one).
90
98
  */
91
99
  export function isPrivilegedScheme(url: Platform.DevToolsPath.UrlString|URL|string): boolean {
92
- try {
93
- const parsed = new URL(url);
94
- if (parsed.protocol === 'blob:') {
95
- return isPrivilegedScheme(parsed.pathname);
96
- }
97
- return PRIVILEGED_SCHEMES.has(parsed.protocol);
98
- } catch {
99
- return false;
100
- }
100
+ const parsed = parseUnwrappedURL(url);
101
+ return parsed !== null && PRIVILEGED_SCHEMES.has(parsed.protocol);
101
102
  }
102
103
 
103
104
  /**
@@ -1100,8 +1100,9 @@ export class DOMNode extends Common.ObjectWrapper.ObjectWrapper<DOMNodeEventType
1100
1100
  */
1101
1101
  async toggleHideElement(): Promise<void> {
1102
1102
  let pseudoElementName = this.pseudoType() ? this.nodeName() : null;
1103
- if (pseudoElementName && this.pseudoIdentifier()) {
1104
- pseudoElementName += `(${this.pseudoIdentifier()})`;
1103
+ const pseudoIdentifier = this.pseudoIdentifier();
1104
+ if (pseudoElementName && pseudoIdentifier) {
1105
+ pseudoElementName += `(${cssEscape(pseudoIdentifier)})`;
1105
1106
  }
1106
1107
 
1107
1108
  let effectiveNode: DOMNode|null = this;
@@ -1358,8 +1359,9 @@ export class DOMNode extends Common.ObjectWrapper.ObjectWrapper<DOMNodeEventType
1358
1359
  const classList = classes.trim().split(/\s+/g);
1359
1360
  return (lowerCaseName === 'div' ? '' : lowerCaseName) + '.' + classList.map(cls => cssEscape(cls)).join('.');
1360
1361
  }
1361
- if (this.pseudoIdentifier()) {
1362
- return `${lowerCaseName}(${this.pseudoIdentifier()})`;
1362
+ const pseudoIdentifier = this.pseudoIdentifier();
1363
+ if (pseudoIdentifier) {
1364
+ return `${lowerCaseName}(${cssEscape(pseudoIdentifier)})`;
1363
1365
  }
1364
1366
  return lowerCaseName;
1365
1367
  }
@@ -109,7 +109,7 @@ export function toggleClassAndInjectStyleRule(this: Element, pseudoElementName:
109
109
  const classNamePrefix = '__web-inspector-hide';
110
110
  const classNameSuffix = '-shortcut__';
111
111
  const styleTagId = '__web-inspector-hide-shortcut-style__';
112
- const pseudoElementNameEscaped = pseudoElementName ? pseudoElementName.replace(/[\(\)\:]/g, '_') : '';
112
+ const pseudoElementNameEscaped = pseudoElementName ? pseudoElementName.replace(/[^a-zA-Z0-9_-]/g, '_') : '';
113
113
  const className = classNamePrefix + pseudoElementNameEscaped + classNameSuffix;
114
114
  this.classList.toggle(className, hidden);
115
115
 
@@ -257,6 +257,7 @@ inspectorBackend.registerCommand("CSS.disable", [], [], "Disables the CSS agent
257
257
  inspectorBackend.registerCommand("CSS.enable", [], [], "Enables the CSS agent for the given page. Clients should not assume that the CSS agent has been enabled until the result of this command is received.");
258
258
  inspectorBackend.registerCommand("CSS.forcePseudoState", [{"name": "nodeId", "type": "number", "optional": false, "description": "The element id for which to force the pseudo state.", "typeRef": "DOM.NodeId"}, {"name": "forcedPseudoClasses", "type": "array", "optional": false, "description": "Element pseudo classes to force when computing the element's style.", "typeRef": "string"}], [], "Ensures that the given node will have specified pseudo-classes whenever its style is computed by the browser.");
259
259
  inspectorBackend.registerCommand("CSS.forceStartingStyle", [{"name": "nodeId", "type": "number", "optional": false, "description": "The element id for which to force the starting-style state.", "typeRef": "DOM.NodeId"}, {"name": "forced", "type": "boolean", "optional": false, "description": "Boolean indicating if this is on or off.", "typeRef": null}], [], "Ensures that the given node is in its starting-style state.");
260
+ inspectorBackend.registerCommand("CSS.forcePositionTryOption", [{"name": "nodeId", "type": "number", "optional": false, "description": "The element id for which to force the position-try option.", "typeRef": "DOM.NodeId"}, {"name": "index", "type": "number", "optional": true, "description": "The 1-based index of the position-try fallback option, 0 for base position (no fallback), or omitted to clear the forced state.", "typeRef": null}], [], "Forces a position-try option for the given node.");
260
261
  inspectorBackend.registerCommand("CSS.getBackgroundColors", [{"name": "nodeId", "type": "number", "optional": false, "description": "Id of the node to get background colors for.", "typeRef": "DOM.NodeId"}], ["backgroundColors", "computedFontSize", "computedFontWeight"], "");
261
262
  inspectorBackend.registerCommand("CSS.getComputedStyleForNode", [{"name": "nodeId", "type": "number", "optional": false, "description": "", "typeRef": "DOM.NodeId"}], ["computedStyle", "extraFields"], "Returns the computed style for a DOM node identified by `nodeId`.");
262
263
  inspectorBackend.registerCommand("CSS.resolveValues", [{"name": "values", "type": "array", "optional": false, "description": "Cascade-dependent keywords (revert/revert-layer) do not work.", "typeRef": "string"}, {"name": "nodeId", "type": "number", "optional": false, "description": "Id of the node in whose context the expression is evaluated", "typeRef": "DOM.NodeId"}, {"name": "propertyName", "type": "string", "optional": true, "description": "Only longhands and custom property names are accepted.", "typeRef": null}, {"name": "pseudoType", "type": "string", "optional": true, "description": "Pseudo element type, only works for pseudo elements that generate elements in the tree, such as ::before and ::after.", "typeRef": "DOM.PseudoType"}, {"name": "pseudoIdentifier", "type": "string", "optional": true, "description": "Pseudo element custom ident.", "typeRef": null}], ["results"], "Resolve the specified values in the context of the provided element. For example, a value of '1em' is evaluated according to the computed 'font-size' of the element and a value 'calc(1px + 2px)' will be resolved to '3px'. If the `propertyName` was specified the `values` are resolved as if they were property's declaration. If a value cannot be parsed according to the provided property syntax, the value is parsed using combined syntax as if null `propertyName` was provided. If the value cannot be resolved even then, return the provided value without any changes. Note: this function currently does not resolve CSS random() function, it returns unmodified random() function parts.`");
@@ -1567,6 +1567,13 @@ export namespace ProtocolMapping {
1567
1567
  paramsType: [Protocol.CSS.ForceStartingStyleRequest];
1568
1568
  returnType: void;
1569
1569
  };
1570
+ /**
1571
+ * Forces a position-try option for the given node.
1572
+ */
1573
+ 'CSS.forcePositionTryOption': {
1574
+ paramsType: [Protocol.CSS.ForcePositionTryOptionRequest];
1575
+ returnType: void;
1576
+ };
1570
1577
  'CSS.getBackgroundColors': {
1571
1578
  paramsType: [Protocol.CSS.GetBackgroundColorsRequest];
1572
1579
  returnType: Protocol.CSS.GetBackgroundColorsResponse;
@@ -796,6 +796,11 @@ declare namespace ProtocolProxyApi {
796
796
  */
797
797
  invoke_forceStartingStyle(params: Protocol.CSS.ForceStartingStyleRequest): Promise<Protocol.ProtocolResponseWithError>;
798
798
 
799
+ /**
800
+ * Forces a position-try option for the given node.
801
+ */
802
+ invoke_forcePositionTryOption(params: Protocol.CSS.ForcePositionTryOptionRequest): Promise<Protocol.ProtocolResponseWithError>;
803
+
799
804
  invoke_getBackgroundColors(params: Protocol.CSS.GetBackgroundColorsRequest): Promise<Protocol.CSS.GetBackgroundColorsResponse>;
800
805
 
801
806
  /**
@@ -4066,6 +4066,18 @@ export namespace CSS {
4066
4066
  forced: boolean;
4067
4067
  }
4068
4068
 
4069
+ export interface ForcePositionTryOptionRequest {
4070
+ /**
4071
+ * The element id for which to force the position-try option.
4072
+ */
4073
+ nodeId: DOM.NodeId;
4074
+ /**
4075
+ * The 1-based index of the position-try fallback option, 0 for base position (no fallback),
4076
+ * or omitted to clear the forced state.
4077
+ */
4078
+ index?: integer;
4079
+ }
4080
+
4069
4081
  export interface GetBackgroundColorsRequest {
4070
4082
  /**
4071
4083
  * Id of the node to get background colors for.
@@ -189,7 +189,7 @@ export interface ParsedAnswer {
189
189
 
190
190
  export type ParsedResponse = ParsedAnswer;
191
191
 
192
- export const MAX_STEPS = 10;
192
+ export const MAX_STEPS = 20;
193
193
 
194
194
  export interface ConversationSuggestion {
195
195
  title: string;
@@ -519,8 +519,6 @@ class CrossOriginError extends Error {
519
519
  *
520
520
  * TODO: missing a test that action code is yielded before the
521
521
  * confirmation dialog.
522
- * TODO: missing a test for an error if it took
523
- * more than MAX_STEPS iterations.
524
522
  */
525
523
  export abstract class AiAgent<T> {
526
524
  /**
@@ -994,7 +992,12 @@ export abstract class AiAgent<T> {
994
992
  break;
995
993
  }
996
994
  } else {
997
- yield this.#createErrorResponse(i - 1 === MAX_STEPS ? ErrorType.MAX_STEPS : ErrorType.UNKNOWN);
995
+ yield this.#createErrorResponse(ErrorType.UNKNOWN);
996
+ break;
997
+ }
998
+
999
+ if (i === MAX_STEPS - 1) {
1000
+ yield this.#createErrorResponse(ErrorType.MAX_STEPS);
998
1001
  break;
999
1002
  }
1000
1003
  }
@@ -47,6 +47,7 @@ First, examine the provided context, then use the functions to gather additional
47
47
  * After applying a fix, please ask the user to confirm if the fix worked or not.
48
48
  * ALWAYS OUTPUT a list of follow-up queries at the end of your text response. The format is SUGGESTIONS: ["suggestion1", "suggestion2", "suggestion3"]. Make sure that the array and the \`SUGGESTIONS: \` text is in the same line. You're also capable of executing the fix for the issue user mentioned. Reflect this in your suggestions.
49
49
  * Use the precision of Strunk & White, the brevity of Hemingway, and the simple clarity of Vonnegut. Don't add repeated information, and keep the whole answer short.
50
+ * If the target element is in a cross-origin iframe, stop and ask the user to select it in the Elements panel and start a new chat with that context.
50
51
  * **CRITICAL** NEVER write full Python programs - you should only write individual statements that invoke a single function from the provided library.
51
52
  * **CRITICAL** NEVER output text before a function call. Always do a function call first.
52
53
  * **CRITICAL** When answering questions about positioning or layout, ALWAYS inspect \`position\`, \`display\` and all other related properties. You MUST provide a specific list of CSS property names when calling functions to get styles. Do not use generic values like "all" or "*".
@@ -34,6 +34,11 @@ Lighthouse Version: 1.0.0
34
34
 
35
35
  ## Category Scores
36
36
  - Accessibility: 50
37
+
38
+ ## Failing audits
39
+
40
+ ### Accessibility (categoryId: "accessibility")
41
+ - Accessibility Audit: 50
37
42
  === end content
38
43
 
39
44
  Title: LighthouseContext with AI V2 architecture enabled returns the report summary as user facing details
@@ -41,7 +46,7 @@ Content:
41
46
  [
42
47
  {
43
48
  "title": "Lighthouse report",
44
- "text": "# Lighthouse Report Summary\nURL: https://example.com\nFetch Time: 2026-03-12\nLighthouse Version: 1.0.0\n\n## Category Scores\n- Accessibility: 50"
49
+ "text": "# Lighthouse Report Summary\nURL: https://example.com\nFetch Time: 2026-03-12\nLighthouse Version: 1.0.0\n\n## Category Scores\n- Accessibility: 50\n\n## Failing audits\n\n### Accessibility (categoryId: \"accessibility\")\n- Accessibility Audit: 50"
45
50
  }
46
51
  ]
47
52
  === end content
@@ -58,8 +58,9 @@ export class LighthouseContext extends ConversationContext<LHModel.ReporterTypes
58
58
  this.#cachedPayload =
59
59
  '**CRITICAL**: The Lighthouse report failed to record or all category scores are error/unavailable (n/a). This indicates a failed run or missing data.';
60
60
  } else if (Root.Runtime.hostConfig.devToolsAiV2Architecture?.enabled) {
61
- // AI V2 sends only the summary; the agent fetches category audits on demand via `getLighthouseAudits`.
62
- this.#cachedPayload = formatter.summary(this.#lh);
61
+ // AI V2 sends failing audit titles without their details to keep the prompt small. The agent
62
+ // uses the category IDs in the list to fetch full details with `getLighthouseAudits`.
63
+ this.#cachedPayload = `${formatter.summary(this.#lh)}\n\n${formatter.failingAuditsSummary(this.#lh)}`;
63
64
  } else {
64
65
  // The V1 `AccessibilityAgent` expects the accessibility audits up front. Remove this branch with V1.
65
66
  this.#cachedPayload =
@@ -133,3 +133,14 @@ The following audits in this category have a score below 90 and may need attenti
133
133
  * **Resource Size**: 22.2 kB
134
134
  * **Est Savings**: 16.9 kB
135
135
  === end content
136
+
137
+ Title: LighthouseFormatter lists failing audits grouped by category with the category ID
138
+ Content:
139
+ ## Failing audits
140
+
141
+ ### Performance (categoryId: "performance")
142
+ - First Audit: 80
143
+
144
+ ### Accessibility (categoryId: "accessibility")
145
+ - No failing audits.
146
+ === end content
@@ -36,17 +36,37 @@ export class LighthouseFormatter {
36
36
  }
37
37
 
38
38
  /**
39
- * Formats a Lighthouse report for an AI Agent. If categoryId is 'all', includes
40
- * the overall summary followed by each category's audits. Otherwise, returns audits
41
- * for the specified category.
39
+ * Returns the title and score of every failing audit (score < 90), grouped by category.
40
+ * Descriptions and details tables are left out to keep the prompt small. Each category
41
+ * heading includes its category ID so the agent can request the full audit details.
42
+ */
43
+ failingAuditsSummary(report: LHModel.ReporterTypes.ReportJSON): string {
44
+ const lines: string[] = [];
45
+ lines.push('## Failing audits');
46
+ for (const [categoryId, category] of Object.entries(report.categories)) {
47
+ lines.push('');
48
+ lines.push(`### ${category.title} (categoryId: "${categoryId}")`);
49
+ const failingAudits = this.#findFailingAudits(report, category);
50
+ if (failingAudits.length === 0) {
51
+ lines.push('- No failing audits.');
52
+ continue;
53
+ }
54
+ for (const audit of failingAudits) {
55
+ lines.push(`- ${audit.title}: ${this.#formatScore(audit.score)}`);
56
+ }
57
+ }
58
+ return lines.join('\n');
59
+ }
60
+
61
+ /**
62
+ * Formats a Lighthouse report for an AI Agent. If categoryId is 'all', returns
63
+ * each category's audits. Otherwise, returns audits for the specified category.
64
+ * The output does not include the report summary, because the conversation
65
+ * context already sends it.
42
66
  */
43
67
  formatReport(report: LHModel.ReporterTypes.ReportJSON, categoryId: LighthouseCategoryArg): string {
44
68
  if (categoryId === 'all') {
45
- const sections: string[] = [this.summary(report)];
46
- for (const category of Object.values(report.categories)) {
47
- sections.push(this.audits(report, category));
48
- }
49
- return sections.join('\n\n');
69
+ return Object.values(report.categories).map(category => this.audits(report, category)).join('\n\n');
50
70
  }
51
71
  return this.audits(report, categoryId);
52
72
  }
@@ -69,10 +89,7 @@ export class LighthouseFormatter {
69
89
  }
70
90
  lines.push('');
71
91
 
72
- const failingAudits = category.auditRefs.filter(ref => {
73
- const audit = report.audits[ref.id];
74
- return audit && audit.score !== null && audit.score < 0.9;
75
- });
92
+ const failingAudits = this.#findFailingAudits(report, category);
76
93
 
77
94
  if (failingAudits.length === 0) {
78
95
  lines.push('All audits in this category passed (score >= 90).');
@@ -80,13 +97,8 @@ export class LighthouseFormatter {
80
97
  }
81
98
 
82
99
  lines.push('The following audits in this category have a score below 90 and may need attention:');
83
- for (const ref of failingAudits) {
84
- const audit = report.audits[ref.id];
85
- if (!audit) {
86
- continue;
87
- }
88
- const score = audit.score !== null ? Math.round(audit.score * 100) : 'n/a';
89
- let line = `- **${audit.title}**: ${score}`;
100
+ for (const audit of failingAudits) {
101
+ let line = `- **${audit.title}**: ${this.#formatScore(audit.score)}`;
90
102
  if (audit.displayValue) {
91
103
  line += ` (${audit.displayValue})`;
92
104
  }
@@ -104,6 +116,22 @@ export class LighthouseFormatter {
104
116
  return lines.join('\n');
105
117
  }
106
118
 
119
+ /**
120
+ * Returns the audits in a category that scored below 90, in the category's order.
121
+ * Audits without a score (such as informative or manual audits) cannot fail, so they
122
+ * are excluded.
123
+ */
124
+ #findFailingAudits(report: LHModel.ReporterTypes.ReportJSON, category: LHModel.ReporterTypes.CategoryJSON):
125
+ Array<LHModel.ReporterTypes.AuditResultJSON&{score: number}> {
126
+ return category.auditRefs.map(ref => report.audits[ref.id])
127
+ .filter((audit): audit is LHModel.ReporterTypes.AuditResultJSON&{score: number} =>
128
+ Boolean(audit) && audit.score !== null && audit.score < 0.9);
129
+ }
130
+
131
+ #formatScore(score: number): number {
132
+ return Math.round(score * 100);
133
+ }
134
+
107
135
  #formatDetails(details: LHModel.ReporterTypes.DetailsJSON): string {
108
136
  switch (details.type) {
109
137
  case 'table': {
@@ -19,6 +19,7 @@ You are an expert accessibility debugging assistant.
19
19
  - Use `getStyles` on the backend node ID to inspect layout, color contrast, or font properties.
20
20
 
21
21
  2. **Lighthouse Accessibility Audits (`getLighthouseAudits` & `runLighthouse`)**:
22
+ - The Lighthouse report context contains only category scores and failing audit titles. To answer questions about failing accessibility audits, you must first call `getLighthouseAudits` with `categoryId: 'accessibility'` to fetch the full details. Never reply using only the initial summary.
22
23
  - If the user asks for a Lighthouse audit or report (such as recording a report or checking accessibility scores), or if audits are needed:
23
24
  - If an active Lighthouse report context already exists and no fresh audit is requested, query it via `getLighthouseAudits` with `categoryId: 'accessibility'`.
24
25
  - If no active report exists or a fresh audit is requested, use `runLighthouse` with `categoryId: 'accessibility'`:
@@ -12,6 +12,7 @@ Your role is to evaluate websites using Lighthouse audits across performance, ac
12
12
  # Tools & Workflow
13
13
 
14
14
  1. **Lighthouse Audits (`runLighthouse` & `getLighthouseAudits`)**:
15
+ - The Lighthouse report context contains only category scores and failing audit titles. To answer questions about failing audits or how to improve a score, you must first call `getLighthouseAudits` for the relevant category to fetch the full details. Never reply using only the initial summary.
15
16
  - If an active Lighthouse report context already exists and no fresh audit is requested:
16
17
  - To inspect the entire report or multiple categories, call `getLighthouseAudits` with `categoryId: 'all'`.
17
18
  - For a specific category, call `getLighthouseAudits` with the corresponding category ID (e.g. `'performance'`, `'accessibility'`, `'best-practices'`, `'seo'`).
@@ -39,3 +39,4 @@ The user selected a DOM element in DevTools and asks a query about the element o
39
39
 
40
40
  * Meticulously investigate all potential causes for the observed behavior before concluding. Inspect parents, siblings, children, and overlapping elements where relevant.
41
41
  * After applying a style fix, ask the user to verify if the visual change resolved their issue.
42
+ * If the target element is in a cross-origin iframe, stop and ask the user to select it in the Elements panel and start a new chat with that context.
@@ -43,10 +43,13 @@ export class ListSourcesTool implements
43
43
  static lastSourceId = 0;
44
44
  static uiSourceCodeId: WeakMap<Workspace.UISourceCode.UISourceCode, number> =
45
45
  new WeakMap<Workspace.UISourceCode.UISourceCode, number>();
46
+ static idToUiSourceCode: Map<number, Workspace.UISourceCode.UISourceCode> =
47
+ new Map<number, Workspace.UISourceCode.UISourceCode>();
46
48
 
47
49
  static reset(): void {
48
50
  ListSourcesTool.lastSourceId = 0;
49
51
  ListSourcesTool.uiSourceCodeId = new WeakMap();
52
+ ListSourcesTool.idToUiSourceCode = new Map();
50
53
  }
51
54
 
52
55
  static getUISourceCodes(
@@ -78,7 +81,9 @@ export class ListSourcesTool implements
78
81
  if (!uiSourceCodes.get(url) || uiSourceCode.contentType().isFromSourceMap()) {
79
82
  uiSourceCodes.set(url, uiSourceCode);
80
83
  if (!ListSourcesTool.uiSourceCodeId.has(uiSourceCode)) {
81
- ListSourcesTool.uiSourceCodeId.set(uiSourceCode, ++ListSourcesTool.lastSourceId);
84
+ const id = ++ListSourcesTool.lastSourceId;
85
+ ListSourcesTool.uiSourceCodeId.set(uiSourceCode, id);
86
+ ListSourcesTool.idToUiSourceCode.set(id, uiSourceCode);
82
87
  }
83
88
  }
84
89
  }
@@ -90,14 +95,15 @@ export class ListSourcesTool implements
90
95
  static getSourceById(
91
96
  id: number,
92
97
  originLock: OriginLockState,
93
- // eslint-disable-next-line @devtools/no-instance-of-migrated-singletons
94
- workspace: Workspace.Workspace.WorkspaceImpl = Workspace.Workspace.WorkspaceImpl.instance(),
95
98
  ): Workspace.UISourceCode.UISourceCode|undefined {
96
99
  if (!Number.isInteger(id) || id <= 0) {
97
100
  return undefined;
98
101
  }
99
- return ListSourcesTool.getUISourceCodes(originLock, workspace)
100
- .find(file => ListSourcesTool.uiSourceCodeId.get(file) === id);
102
+ const file = ListSourcesTool.idToUiSourceCode.get(id);
103
+ if (!file || !isOriginAllowedByLock(originLock, file.securityOrigin())) {
104
+ return undefined;
105
+ }
106
+ return file;
101
107
  }
102
108
 
103
109
  readonly parameters: Host.AidaClient.FunctionObjectParam<never> = {
@@ -144,6 +144,10 @@ export class DebuggerWorkspaceBinding implements SDK.TargetManager.SDKModelObser
144
144
  }
145
145
  ranges = compilerMapping.getLocationRangesForSameSourceLocation(rawLocation);
146
146
  ranges = ranges.filter(range => contained(rawLocation, range));
147
+ if (mode === SDK.DebuggerModel.StepMode.STEP_OVER) {
148
+ // Step over functions inlined by the compiler (from encoded source map scopes).
149
+ ranges = ranges.concat(SourceMapStepping.inlinedCalleeRanges(callFrame));
150
+ }
147
151
  return ranges;
148
152
  }
149
153
 
@@ -516,7 +520,7 @@ export class DebuggerWorkspaceBinding implements SDK.TargetManager.SDKModelObser
516
520
  null :
517
521
  await this.computeAutoStep(SDK.DebuggerModel.StepMode.STEP_OVER, callFrames);
518
522
  }
519
- return null;
523
+ return await SourceMapStepping.nextAutoStep(debuggerPausedDetails, context, this.computeAutoStep.bind(this));
520
524
  }
521
525
 
522
526
  async #shouldPauseInWasm(debuggerPausedDetails: SDK.DebuggerModel.DebuggerPausedDetails,
@@ -5,11 +5,13 @@
5
5
  /**
6
6
  * @file Stepping through code with encoded source map scopes: inlined functions, outlined functions and unmapped code.
7
7
  *
8
- * Mirrors the Wasm auto-stepping: user steps get a skip list (see `DebuggerWorkspaceBinding.computeAutoStep`).
8
+ * Mirrors the Wasm auto-stepping: user steps get a skip list (see `DebuggerWorkspaceBinding.computeAutoStep`), and
9
+ * pauses that don't correspond to a "logical" step are continued automatically ({@link nextAutoStep}).
9
10
  */
10
11
 
11
12
  import * as Root from '../../core/root/root.js';
12
- import type * as SDK from '../../core/sdk/sdk.js';
13
+ import * as SDK from '../../core/sdk/sdk.js';
14
+ import * as Protocol from '../../generated/protocol.js';
13
15
 
14
16
  interface ScopedPosition {
15
17
  sourceMap: SDK.SourceMap.SourceMap;
@@ -19,14 +21,17 @@ interface ScopedPosition {
19
21
  column: number;
20
22
  }
21
23
 
22
- /** @returns the script-relative position of {@link frame}, if it's in a script with encoded source map scopes. */
24
+ /**
25
+ * @returns the script-relative position of {@link frame}, if it's in a script with encoded source map scopes.
26
+ * Scopes without any mappings are ignored: every position would count as unmapped (see {@link isUnmapped}).
27
+ */
23
28
  function scopedPosition(frame: SDK.DebuggerModel.CallFrame): ScopedPosition|null {
24
29
  if (!Root.Runtime.hostConfig.devToolsSourceMapScopesInSourcesPanel?.enabled) {
25
30
  return null;
26
31
  }
27
32
  const script = frame.script;
28
33
  const sourceMap = script.sourceMap();
29
- if (!sourceMap?.hasEncodedScopeInfo()) {
34
+ if (!sourceMap?.hasEncodedScopeInfo() || sourceMap.mappings().length === 0) {
30
35
  return null;
31
36
  }
32
37
  const {lineNumber, columnNumber} = script.rawLocationToRelativeLocation(frame.location());
@@ -42,9 +47,76 @@ function toLocationRanges({script}: ScopedPosition,
42
47
  return ranges.map(({start, end}) => ({start: toLocation(start), end: toLocation(end)}));
43
48
  }
44
49
 
50
+ export function isScopedFrame(frame: SDK.DebuggerModel.CallFrame): boolean {
51
+ return scopedPosition(frame) !== null;
52
+ }
53
+
54
+ /** @returns true iff {@link frame} has encoded scopes, and its position is not mapped to any source. */
55
+ export function isUnmapped(frame: SDK.DebuggerModel.CallFrame): boolean {
56
+ const position = scopedPosition(frame);
57
+ return position !== null && position.sourceMap.findEntry(position.line, position.column)?.sourceURL === undefined;
58
+ }
59
+
60
+ /** @returns true iff both frames have encoded scopes and are mapped to the same original location. */
61
+ export function isSameOriginalLocation(a: SDK.DebuggerModel.CallFrame, b: SDK.DebuggerModel.CallFrame): boolean {
62
+ const entry = (frame: SDK.DebuggerModel.CallFrame): SDK.SourceMap.SourceMapEntry|null => {
63
+ const position = scopedPosition(frame);
64
+ return position?.sourceMap.findEntry(position.line, position.column) ?? null;
65
+ };
66
+ const entryA = entry(a);
67
+ const entryB = entry(b);
68
+ return entryA?.sourceURL !== undefined && entryA.sourceURL === entryB?.sourceURL &&
69
+ entryA.sourceLineNumber === entryB.sourceLineNumber && entryA.sourceColumnNumber === entryB.sourceColumnNumber;
70
+ }
71
+
45
72
  /** @returns the body of the innermost inlined function that {@link frame} is paused in (empty if none). */
46
73
  export function inlinedFunctionRanges(frame: SDK.DebuggerModel.CallFrame): SDK.DebuggerModel.LocationRange[] {
47
74
  const position = scopedPosition(frame);
48
75
  const range = position?.sourceMap.inlinedFunctionRange(position.line, position.column);
49
76
  return position && range ? toLocationRanges(position, [range]) : [];
50
77
  }
78
+
79
+ /** @returns the bodies of the functions inlined into the logical function that {@link frame} is paused in. */
80
+ export function inlinedCalleeRanges(frame: SDK.DebuggerModel.CallFrame): SDK.DebuggerModel.LocationRange[] {
81
+ const position = scopedPosition(frame);
82
+ return position ? toLocationRanges(position, position.sourceMap.inlinedCalleeRanges(position.line, position.column)) :
83
+ [];
84
+ }
85
+
86
+ /**
87
+ * Decides whether the pause {@link details} completes the user's step {@link context}.
88
+ *
89
+ * @returns null to present the pause, or the step to issue instead.
90
+ */
91
+ export async function nextAutoStep(
92
+ details: SDK.DebuggerModel.DebuggerPausedDetails, context: SDK.DebuggerModel.StepContext|null,
93
+ computeAutoStep: SDK.DebuggerModel.ComputeAutoStepCallback): Promise<SDK.DebuggerModel.AutoStep|null> {
94
+ if (!context || details.reason !== Protocol.Debugger.PausedEventReason.Step) {
95
+ return null;
96
+ }
97
+ const frames = details.callFrames;
98
+ const start = context.callFrames;
99
+ if (frames.length === 0 || start.length === 0 || (!isScopedFrame(frames[0]) && !isScopedFrame(start[0]))) {
100
+ return null;
101
+ }
102
+
103
+ switch (context.mode) {
104
+ case SDK.DebuggerModel.StepMode.STEP_INTO:
105
+ return isUnmapped(frames[0]) ? await computeAutoStep(SDK.DebuggerModel.StepMode.STEP_INTO, frames) : null;
106
+
107
+ case SDK.DebuggerModel.StepMode.STEP_OVER: {
108
+ const startDepth = start.length;
109
+ const depth = frames.length;
110
+ if (isUnmapped(frames[0]) || (depth === startDepth && isSameOriginalLocation(frames[0], start[0]))) {
111
+ // Unmapped code, or still on the same original location (the skip list only covers the generated ranges that
112
+ // contain the start position): keep stepping over.
113
+ return await computeAutoStep(SDK.DebuggerModel.StepMode.STEP_OVER, frames);
114
+ }
115
+ return null;
116
+ }
117
+
118
+ case SDK.DebuggerModel.StepMode.STEP_OUT:
119
+ // Stepping into calls of the unmapped code would present a pause inside the callee, so continue as step over.
120
+ return isUnmapped(frames[0]) ? await computeAutoStep(SDK.DebuggerModel.StepMode.STEP_OVER, frames) : null;
121
+ }
122
+ }
@@ -191,6 +191,9 @@ export class StyleFile implements TextUtils.ContentProvider.ContentProvider {
191
191
 
192
192
  addHeader(header: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader): void {
193
193
  this.headers.add(header);
194
+ if (header.hasSourceURL) {
195
+ NetworkProject.setSourceURLSynthesized(this.uiSourceCode);
196
+ }
194
197
  NetworkProject.addFrameAttribution(this.uiSourceCode, header.frameId);
195
198
  }
196
199
 
@@ -319,6 +319,8 @@ export class DeviceModeModel extends Common.ObjectWrapper.ObjectWrapper<EventTyp
319
319
  if (this.#type === Type.Device && this.#device && this.#mode) {
320
320
  const orientation = this.#device.orientationByName(this.#mode.orientation);
321
321
  this.#scaleSetting.set(this.calculateFitScale(orientation.width, orientation.height));
322
+ } else if (this.#type === Type.Responsive) {
323
+ this.#scaleSetting.set(this.calculateFitScale(this.#widthSetting.get(), this.#heightSetting.get()));
322
324
  }
323
325
  }
324
326
 
@@ -377,7 +379,7 @@ export class DeviceModeModel extends Common.ObjectWrapper.ObjectWrapper<EventTyp
377
379
  if (savedDevice) {
378
380
  this.#autoAdjustScaleSetting.set(savedDevice.autoAdjust);
379
381
  // An explicitly requested scale takes precedence over the saved one.
380
- if (scale === undefined && !savedDevice.autoAdjust) {
382
+ if (scale === undefined && !savedDevice.autoAdjust && savedDevice.scale > 0) {
381
383
  scale = savedDevice.scale;
382
384
  }
383
385
  } else {
@@ -404,10 +406,13 @@ export class DeviceModeModel extends Common.ObjectWrapper.ObjectWrapper<EventTyp
404
406
  const savedDevice = map['Responsive'];
405
407
  if (savedDevice) {
406
408
  this.#autoAdjustScaleSetting.set(savedDevice.autoAdjust);
407
- if (!savedDevice.autoAdjust) {
409
+ if (!savedDevice.autoAdjust && savedDevice.scale > 0) {
408
410
  this.#scaleSetting.set(savedDevice.scale);
411
+ } else if (this.#initialized) {
412
+ this.#updateFitScale();
409
413
  } else {
410
- this.#scaleSetting.set(this.calculateFitScale(this.#widthSetting.get(), this.#heightSetting.get()));
414
+ // Defer fit-scale calculation until setAvailableSize() initializes #preferredSize.
415
+ this.#autoFitScaleOnInitialize = true;
411
416
  }
412
417
  } else {
413
418
  this.#autoAdjustScaleSetting.set(true);
@@ -775,7 +780,7 @@ export class DeviceModeModel extends Common.ObjectWrapper.ObjectWrapper<EventTyp
775
780
  private calculateFitScale(screenWidth: number, screenHeight: number): number {
776
781
  let scale = Math.min(screenWidth ? this.#preferredSize.width / screenWidth : 1,
777
782
  screenHeight ? this.#preferredSize.height / screenHeight : 1);
778
- scale = Math.min(Math.floor(scale * 100), 100);
783
+ scale = Math.max(Math.min(Math.floor(scale * 100), 100), 1);
779
784
 
780
785
  let sharpScale = scale;
781
786
  while (sharpScale > scale * 0.7) {