chrome-devtools-frontend 1.0.1516909 → 1.0.1519267

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 (162) hide show
  1. package/config/owner/COMMON_OWNERS +2 -2
  2. package/docs/checklist/README.md +2 -2
  3. package/docs/checklist/javascript.md +1 -1
  4. package/docs/contributing/README.md +1 -1
  5. package/docs/contributing/settings-experiments-features.md +9 -8
  6. package/docs/cookbook/devtools_on_devtools.md +2 -2
  7. package/docs/cookbook/localization.md +10 -10
  8. package/docs/devtools-protocol.md +9 -8
  9. package/docs/ecosystem/automatic_workspace_folders.md +3 -3
  10. package/docs/get_the_code.md +0 -2
  11. package/docs/styleguide/ux/components.md +166 -85
  12. package/docs/styleguide/ux/numbers.md +3 -4
  13. package/eslint.config.mjs +1 -0
  14. package/front_end/core/common/README.md +13 -12
  15. package/front_end/core/host/GdpClient.ts +16 -1
  16. package/front_end/core/host/UserMetrics.ts +4 -2
  17. package/front_end/core/root/Runtime.ts +13 -0
  18. package/front_end/core/sdk/CSSMatchedStyles.ts +5 -1
  19. package/front_end/core/sdk/EnhancedTracesParser.ts +5 -5
  20. package/front_end/core/sdk/RehydratingConnection.snapshot.txt +211 -0
  21. package/front_end/core/sdk/TargetManager.ts +4 -0
  22. package/front_end/entrypoints/main/MainImpl.ts +6 -3
  23. package/front_end/generated/InspectorBackendCommands.js +10 -7
  24. package/front_end/generated/SupportedCSSProperties.js +40 -11
  25. package/front_end/generated/protocol-mapping.d.ts +16 -1
  26. package/front_end/generated/protocol-proxy-api.d.ts +13 -1
  27. package/front_end/generated/protocol.ts +95 -0
  28. package/front_end/models/ai_assistance/agents/AiAgent.ts +57 -10
  29. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +119 -51
  30. package/front_end/models/ai_assistance/agents/StylingAgent.ts +0 -31
  31. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.snapshot.txt +14 -181
  32. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.ts +19 -315
  33. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.snapshot.txt +224 -50
  34. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.ts +310 -11
  35. package/front_end/models/ai_assistance/performance/AIContext.ts +15 -2
  36. package/front_end/models/ai_code_completion/AiCodeCompletion.ts +22 -11
  37. package/front_end/models/badges/AiExplorerBadge.ts +19 -3
  38. package/front_end/models/badges/Badge.ts +10 -3
  39. package/front_end/models/badges/CodeWhispererBadge.ts +3 -4
  40. package/front_end/models/badges/DOMDetectiveBadge.ts +1 -0
  41. package/front_end/models/badges/SpeedsterBadge.ts +1 -0
  42. package/front_end/models/badges/StarterBadge.ts +3 -2
  43. package/front_end/models/badges/UserBadges.ts +21 -3
  44. package/front_end/models/badges/badges.ts +1 -0
  45. package/front_end/models/javascript_metadata/NativeFunctions.js +2 -2
  46. package/front_end/models/trace/EventsSerializer.ts +4 -3
  47. package/front_end/models/trace/README.md +28 -1
  48. package/front_end/models/trace/handlers/UserInteractionsHandler.ts +101 -73
  49. package/front_end/models/trace/handlers/UserTimingsHandler.ts +1 -1
  50. package/front_end/models/trace/helpers/Timing.ts +1 -1
  51. package/front_end/models/trace/helpers/Trace.ts +99 -43
  52. package/front_end/models/trace/types/TraceEvents.ts +9 -0
  53. package/front_end/panels/accessibility/ARIAAttributesView.ts +113 -191
  54. package/front_end/panels/accessibility/AccessibilityNodeView.ts +9 -9
  55. package/front_end/panels/accessibility/AccessibilitySubPane.ts +6 -4
  56. package/front_end/panels/accessibility/accessibilityProperties.css +2 -0
  57. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +20 -3
  58. package/front_end/panels/ai_assistance/components/ChatView.ts +9 -10
  59. package/front_end/panels/ai_assistance/components/PerformanceAgentMarkdownRenderer.ts +44 -0
  60. package/front_end/panels/application/components/BounceTrackingMitigationsView.ts +2 -2
  61. package/front_end/panels/common/AiCodeCompletionDisclaimer.ts +32 -9
  62. package/front_end/panels/common/AiCodeCompletionSummaryToolbar.ts +7 -1
  63. package/front_end/panels/common/BadgeNotification.ts +21 -5
  64. package/front_end/panels/common/GdpSignUpDialog.ts +20 -12
  65. package/front_end/panels/console/ConsolePrompt.ts +1 -1
  66. package/front_end/panels/console/ConsoleView.ts +6 -2
  67. package/front_end/panels/css_overview/CSSOverviewCompletedView.ts +5 -5
  68. package/front_end/panels/elements/ElementsPanel.ts +4 -0
  69. package/front_end/panels/elements/ElementsTreeElement.ts +18 -0
  70. package/front_end/panels/elements/ElementsTreeOutline.ts +13 -0
  71. package/front_end/panels/elements/StylePropertyTreeElement.ts +21 -6
  72. package/front_end/panels/media/TickingFlameChart.ts +1 -1
  73. package/front_end/panels/profiler/HeapSnapshotView.ts +34 -19
  74. package/front_end/panels/recorder/components/RecordingView.ts +2 -2
  75. package/front_end/panels/search/SearchResultsPane.ts +167 -152
  76. package/front_end/panels/search/SearchView.ts +36 -26
  77. package/front_end/panels/search/searchResultsPane.css +9 -0
  78. package/front_end/panels/security/CookieControlsView.ts +2 -1
  79. package/front_end/panels/settings/AISettingsTab.ts +6 -3
  80. package/front_end/panels/settings/components/SyncSection.ts +39 -17
  81. package/front_end/panels/settings/emulation/components/UserAgentClientHintsForm.ts +1 -1
  82. package/front_end/panels/sources/AiCodeCompletionPlugin.ts +9 -1
  83. package/front_end/panels/sources/SourcesPanel.ts +4 -1
  84. package/front_end/panels/sources/sourcesView.css +6 -1
  85. package/front_end/panels/timeline/AppenderUtils.ts +2 -2
  86. package/front_end/panels/timeline/ExtensionTrackAppender.ts +13 -4
  87. package/front_end/panels/timeline/GPUTrackAppender.ts +2 -1
  88. package/front_end/panels/timeline/InteractionsTrackAppender.ts +5 -1
  89. package/front_end/panels/timeline/LayoutShiftsTrackAppender.ts +2 -1
  90. package/front_end/panels/timeline/ThreadAppender.ts +12 -3
  91. package/front_end/panels/timeline/TimelineFlameChartDataProvider.ts +9 -4
  92. package/front_end/panels/timeline/TimelinePanel.ts +3 -2
  93. package/front_end/panels/timeline/TimelineUIUtils.ts +5 -4
  94. package/front_end/panels/timeline/TimingsTrackAppender.ts +6 -1
  95. package/front_end/panels/timeline/components/CPUThrottlingSelector.ts +95 -82
  96. package/front_end/panels/timeline/components/LayoutShiftDetails.ts +1 -1
  97. package/front_end/panels/timeline/components/LiveMetricsView.ts +2 -2
  98. package/front_end/panels/timeline/components/NetworkRequestDetails.ts +1 -1
  99. package/front_end/panels/timeline/components/RelatedInsightChips.ts +1 -1
  100. package/front_end/panels/timeline/components/SidebarSingleInsightSet.ts +1 -1
  101. package/front_end/panels/timeline/components/cpuThrottlingSelector.css +17 -15
  102. package/front_end/panels/timeline/components/insights/BaseInsightComponent.ts +3 -0
  103. package/front_end/third_party/chromium/README.chromium +1 -1
  104. package/front_end/third_party/codemirror.next/chunk/codemirror.js +1 -1
  105. package/front_end/third_party/codemirror.next/chunk/codemirror.js.map +1 -1
  106. package/front_end/third_party/codemirror.next/codemirror.next.d.ts +6 -9
  107. package/front_end/third_party/codemirror.next/package.json +2 -1
  108. package/front_end/third_party/diff/README.chromium +1 -0
  109. package/front_end/third_party/puppeteer/README.chromium +2 -2
  110. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/core/Realm.d.ts +2 -2
  111. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/injected.d.ts +1 -1
  112. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/injected.js +1 -1
  113. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/version.d.ts +1 -1
  114. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/version.js +1 -1
  115. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/injected/injected.d.ts +1 -1
  116. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/node/ChromeLauncher.d.ts.map +1 -1
  117. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/node/ChromeLauncher.js +1 -0
  118. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/node/ChromeLauncher.js.map +1 -1
  119. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.d.ts +3 -3
  120. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.js +3 -3
  121. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.js.map +1 -1
  122. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/Function.d.ts.map +1 -1
  123. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/Function.js +16 -25
  124. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/Function.js.map +1 -1
  125. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/Mutex.d.ts +2 -2
  126. package/front_end/third_party/puppeteer/package/lib/es5-iife/puppeteer-core-browser.js +19 -28
  127. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/injected.d.ts +1 -1
  128. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/injected.js +1 -1
  129. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/version.d.ts +1 -1
  130. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/version.js +1 -1
  131. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/node/ChromeLauncher.d.ts.map +1 -1
  132. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/node/ChromeLauncher.js +1 -0
  133. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/node/ChromeLauncher.js.map +1 -1
  134. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.d.ts +3 -3
  135. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.js +3 -3
  136. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.js.map +1 -1
  137. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/util/Function.d.ts.map +1 -1
  138. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/util/Function.js +16 -25
  139. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/util/Function.js.map +1 -1
  140. package/front_end/third_party/puppeteer/package/package.json +10 -3
  141. package/front_end/third_party/puppeteer/package/src/generated/injected.ts +1 -1
  142. package/front_end/third_party/puppeteer/package/src/generated/version.ts +1 -1
  143. package/front_end/third_party/puppeteer/package/src/node/ChromeLauncher.ts +1 -0
  144. package/front_end/third_party/puppeteer/package/src/revisions.ts +3 -3
  145. package/front_end/third_party/puppeteer/package/src/util/Function.ts +22 -30
  146. package/front_end/ui/components/dialogs/Dialog.ts +1 -1
  147. package/front_end/ui/components/markdown_view/MarkdownImage.ts +4 -5
  148. package/front_end/ui/components/switch/SwitchImpl.ts +12 -1
  149. package/front_end/ui/components/text_editor/config.ts +22 -9
  150. package/front_end/ui/components/tooltips/Tooltip.ts +70 -31
  151. package/front_end/ui/legacy/README.md +33 -24
  152. package/front_end/ui/legacy/SearchableView.ts +19 -26
  153. package/front_end/ui/legacy/TextPrompt.ts +166 -1
  154. package/front_end/ui/legacy/Treeoutline.ts +19 -3
  155. package/front_end/ui/legacy/UIUtils.ts +15 -2
  156. package/front_end/ui/legacy/XElement.ts +0 -43
  157. package/front_end/ui/legacy/components/perf_ui/FlameChart.ts +20 -4
  158. package/front_end/ui/legacy/components/source_frame/XMLView.ts +12 -11
  159. package/front_end/ui/lit/i18n-template.ts +5 -2
  160. package/front_end/ui/visual_logging/KnownContextValues.ts +23 -6
  161. package/front_end/ui/visual_logging/README.md +43 -27
  162. package/package.json +1 -1
@@ -1082,8 +1082,10 @@ export namespace Audits {
1082
1082
  WriteErrorInsufficientResources = 'WriteErrorInsufficientResources',
1083
1083
  WriteErrorInvalidMatchField = 'WriteErrorInvalidMatchField',
1084
1084
  WriteErrorInvalidStructuredHeader = 'WriteErrorInvalidStructuredHeader',
1085
+ WriteErrorInvalidTTLField = 'WriteErrorInvalidTTLField',
1085
1086
  WriteErrorNavigationRequest = 'WriteErrorNavigationRequest',
1086
1087
  WriteErrorNoMatchField = 'WriteErrorNoMatchField',
1088
+ WriteErrorNonIntegerTTLField = 'WriteErrorNonIntegerTTLField',
1087
1089
  WriteErrorNonListMatchDestField = 'WriteErrorNonListMatchDestField',
1088
1090
  WriteErrorNonSecureContext = 'WriteErrorNonSecureContext',
1089
1091
  WriteErrorNonStringIdField = 'WriteErrorNonStringIdField',
@@ -9962,6 +9964,10 @@ export namespace Network {
9962
9964
  * request corresponding to the main frame.
9963
9965
  */
9964
9966
  isSameSite?: boolean;
9967
+ /**
9968
+ * True when the resource request is ad-related.
9969
+ */
9970
+ isAdRelated?: boolean;
9965
9971
  }
9966
9972
 
9967
9973
  /**
@@ -10999,6 +11005,43 @@ export namespace Network {
10999
11005
  Zstd = 'zstd',
11000
11006
  }
11001
11007
 
11008
+ export interface NetworkConditions {
11009
+ /**
11010
+ * Only matching requests will be affected by these conditions. Patterns use the URLPattern constructor string
11011
+ * syntax (https://urlpattern.spec.whatwg.org/). If the pattern is empty, all requests are matched (including p2p
11012
+ * connections).
11013
+ */
11014
+ urlPattern: string;
11015
+ /**
11016
+ * Minimum latency from request sent to response headers received (ms).
11017
+ */
11018
+ latency: number;
11019
+ /**
11020
+ * Maximal aggregated download throughput (bytes/sec). -1 disables download throttling.
11021
+ */
11022
+ downloadThroughput: number;
11023
+ /**
11024
+ * Maximal aggregated upload throughput (bytes/sec). -1 disables upload throttling.
11025
+ */
11026
+ uploadThroughput: number;
11027
+ /**
11028
+ * Connection type if known.
11029
+ */
11030
+ connectionType?: ConnectionType;
11031
+ /**
11032
+ * WebRTC packet loss (percent, 0-100). 0 disables packet loss emulation, 100 drops all the packets.
11033
+ */
11034
+ packetLoss?: number;
11035
+ /**
11036
+ * WebRTC packet queue length (packet). 0 removes any queue length limitations.
11037
+ */
11038
+ packetQueueLength?: integer;
11039
+ /**
11040
+ * WebRTC packetReordering feature.
11041
+ */
11042
+ packetReordering?: boolean;
11043
+ }
11044
+
11002
11045
  export const enum DirectSocketDnsQueryType {
11003
11046
  Ipv4 = 'ipv4',
11004
11047
  Ipv6 = 'ipv6',
@@ -11364,6 +11407,50 @@ export namespace Network {
11364
11407
  packetReordering?: boolean;
11365
11408
  }
11366
11409
 
11410
+ export interface EmulateNetworkConditionsByRuleRequest {
11411
+ /**
11412
+ * True to emulate internet disconnection.
11413
+ */
11414
+ offline: boolean;
11415
+ /**
11416
+ * Configure conditions for matching requests. If multiple entries match a request, the first entry wins. Global
11417
+ * conditions can be configured by leaving the urlPattern for the conditions empty. These global conditions are
11418
+ * also applied for throttling of p2p connections.
11419
+ */
11420
+ matchedNetworkConditions: NetworkConditions[];
11421
+ }
11422
+
11423
+ export interface EmulateNetworkConditionsByRuleResponse extends ProtocolResponseWithError {
11424
+ /**
11425
+ * An id for each entry in matchedNetworkConditions. The id will be included in the requestWillBeSentExtraInfo for
11426
+ * requests affected by a rule.
11427
+ */
11428
+ ruleIds: string[];
11429
+ }
11430
+
11431
+ export interface OverrideNetworkStateRequest {
11432
+ /**
11433
+ * True to emulate internet disconnection.
11434
+ */
11435
+ offline: boolean;
11436
+ /**
11437
+ * Minimum latency from request sent to response headers received (ms).
11438
+ */
11439
+ latency: number;
11440
+ /**
11441
+ * Maximal aggregated download throughput (bytes/sec). -1 disables download throttling.
11442
+ */
11443
+ downloadThroughput: number;
11444
+ /**
11445
+ * Maximal aggregated upload throughput (bytes/sec). -1 disables upload throttling.
11446
+ */
11447
+ uploadThroughput: number;
11448
+ /**
11449
+ * Connection type if known.
11450
+ */
11451
+ connectionType?: ConnectionType;
11452
+ }
11453
+
11367
11454
  export interface EnableRequest {
11368
11455
  /**
11369
11456
  * Buffer size in bytes to use when preserving network payloads (XHRs, etc).
@@ -12358,6 +12445,11 @@ export namespace Network {
12358
12445
  * Whether the site has partitioned cookies stored in a partition different than the current one.
12359
12446
  */
12360
12447
  siteHasCookieInOtherPartition?: boolean;
12448
+ /**
12449
+ * The network conditions id if this request was affected by network conditions configured via
12450
+ * emulateNetworkConditionsByRule.
12451
+ */
12452
+ appliedNetworkConditionsId?: string;
12361
12453
  }
12362
12454
 
12363
12455
  /**
@@ -20896,6 +20988,7 @@ export namespace Runtime {
20896
20988
  Dataview = 'dataview',
20897
20989
  Webassemblymemory = 'webassemblymemory',
20898
20990
  Wasmvalue = 'wasmvalue',
20991
+ Trustedtype = 'trustedtype',
20899
20992
  }
20900
20993
 
20901
20994
  /**
@@ -20989,6 +21082,7 @@ export namespace Runtime {
20989
21082
  Dataview = 'dataview',
20990
21083
  Webassemblymemory = 'webassemblymemory',
20991
21084
  Wasmvalue = 'wasmvalue',
21085
+ Trustedtype = 'trustedtype',
20992
21086
  }
20993
21087
 
20994
21088
  /**
@@ -21053,6 +21147,7 @@ export namespace Runtime {
21053
21147
  Dataview = 'dataview',
21054
21148
  Webassemblymemory = 'webassemblymemory',
21055
21149
  Wasmvalue = 'wasmvalue',
21150
+ Trustedtype = 'trustedtype',
21056
21151
  }
21057
21152
 
21058
21153
  export interface PropertyPreview {
@@ -133,13 +133,7 @@ export interface ParsedAnswer {
133
133
  suggestions?: [string, ...string[]];
134
134
  }
135
135
 
136
- export interface ParsedStep {
137
- thought?: string;
138
- title?: string;
139
- action?: string;
140
- }
141
-
142
- export type ParsedResponse = ParsedAnswer|ParsedStep;
136
+ export type ParsedResponse = ParsedAnswer;
143
137
 
144
138
  export const MAX_STEPS = 10;
145
139
 
@@ -404,13 +398,66 @@ export abstract class AiAgent<T> {
404
398
  return this.#origin;
405
399
  }
406
400
 
401
+ /**
402
+ * The AI has instructions to emit structured suggestions in their response. This
403
+ * function parses for that.
404
+ *
405
+ * Note: currently only StylingAgent and PerformanceAgent utilize this, but
406
+ * eventually all agents should support this.
407
+ */
408
+ parseTextResponseForSuggestions(text: string): ParsedResponse {
409
+ if (!text) {
410
+ return {answer: ''};
411
+ }
412
+
413
+ const lines = text.split('\n');
414
+ const answerLines: string[] = [];
415
+ let suggestions: [string, ...string[]]|undefined;
416
+
417
+ for (const line of lines) {
418
+ const trimmed = line.trim();
419
+ if (trimmed.startsWith('SUGGESTIONS:')) {
420
+ try {
421
+ // TODO: Do basic validation this is an array with strings
422
+ suggestions = JSON.parse(trimmed.substring('SUGGESTIONS:'.length).trim());
423
+ } catch {
424
+ }
425
+ } else {
426
+ answerLines.push(line);
427
+ }
428
+ }
429
+
430
+ // Sometimes the model fails to put the SUGGESTIONS text on its own line. Handle
431
+ // the case where the suggestions are part of the last line of the answer.
432
+ if (!suggestions && answerLines.at(-1)?.includes('SUGGESTIONS:')) {
433
+ const [answer, suggestionsText] = answerLines[answerLines.length - 1].split('SUGGESTIONS:', 2);
434
+ try {
435
+ // TODO: Do basic validation this is an array with strings
436
+ suggestions = JSON.parse(suggestionsText.trim().substring('SUGGESTIONS:'.length).trim());
437
+ } catch {
438
+ }
439
+ answerLines[answerLines.length - 1] = answer;
440
+ }
441
+
442
+ const response: ParsedResponse = {
443
+ // If we could not parse the parts, consider the response to be an
444
+ // answer.
445
+ answer: answerLines.join('\n'),
446
+ };
447
+
448
+ if (suggestions) {
449
+ response.suggestions = suggestions;
450
+ }
451
+
452
+ return response;
453
+ }
454
+
407
455
  /**
408
456
  * Parses a streaming text response into a
409
- * though/action/title/answer/suggestions component. This is only used
410
- * by StylingAgent.
457
+ * though/action/title/answer/suggestions component.
411
458
  */
412
459
  parseTextResponse(response: string): ParsedResponse {
413
- return {answer: response};
460
+ return this.parseTextResponseForSuggestions(response.trim());
414
461
  }
415
462
 
416
463
  /**
@@ -2,8 +2,6 @@
2
2
  // Use of this source code is governed by a BSD-style license that can be
3
3
  // found in the LICENSE file.
4
4
 
5
- import '../../../ui/components/icon_button/icon_button.js';
6
-
7
5
  import * as Common from '../../../core/common/common.js';
8
6
  import * as Host from '../../../core/host/host.js';
9
7
  import * as i18n from '../../../core/i18n/i18n.js';
@@ -15,7 +13,6 @@ import * as Trace from '../../trace/trace.js';
15
13
  import {ConversationType} from '../AiHistoryStorage.js';
16
14
  import {
17
15
  PerformanceInsightFormatter,
18
- TraceEventFormatter,
19
16
  } from '../data_formatters/PerformanceInsightFormatter.js';
20
17
  import {PerformanceTraceFormatter} from '../data_formatters/PerformanceTraceFormatter.js';
21
18
  import {debugLog} from '../debug.js';
@@ -62,7 +59,7 @@ const lockedString = i18n.i18n.lockedString;
62
59
  *
63
60
  * Check token length in https://aistudio.google.com/
64
61
  */
65
- const fullTracePreamble = `You are an assistant, expert in web performance and highly skilled with Chrome DevTools.
62
+ const preamble = `You are an assistant, expert in web performance and highly skilled with Chrome DevTools.
66
63
 
67
64
  Your primary goal is to provide actionable advice to web developers about their web page by using the Chrome Performance Panel and analyzing a trace. You may need to diagnose problems yourself, or you may be given direction for what to focus on by the user.
68
65
 
@@ -129,6 +126,16 @@ Adhere to the following critical requirements:
129
126
  - Do not provide answers on non-web-development topics, such as legal, financial, medical, or personal advice.
130
127
  `;
131
128
 
129
+ const extraPreambleWhenNotExternal = `Additional notes:
130
+
131
+ When referring to a trace event that has a corresponding \`eventKey\`, annotate your output using markdown link syntax. For example:
132
+ - When referring to an event that is a long task: [Long task](#r-123)
133
+ - When referring to a URL for which you know the eventKey of: [https://www.example.com](#s-1827)
134
+ - Never show the eventKey (like "eventKey: s-1852"); instead, use a markdown link as described above.
135
+
136
+ When asking the user to make a choice between multiple options, output a list of choices at the end of your text response. The format is \`SUGGESTIONS: ["suggestion1", "suggestion2", "suggestion3"]\`. This MUST start on a newline, and be a single line.
137
+ `;
138
+
132
139
  const callFrameDataFormatDescription = `Each call frame is presented in the following format:
133
140
 
134
141
  'id;name;duration;selfTime;urlIndex;childRange;[S]'
@@ -159,8 +166,8 @@ enum ScorePriority {
159
166
  }
160
167
 
161
168
  export class PerformanceTraceContext extends ConversationContext<AgentFocus> {
162
- static full(parsedTrace: Trace.TraceModel.ParsedTrace): PerformanceTraceContext {
163
- return new PerformanceTraceContext(AgentFocus.full(parsedTrace));
169
+ static fromParsedTrace(parsedTrace: Trace.TraceModel.ParsedTrace): PerformanceTraceContext {
170
+ return new PerformanceTraceContext(AgentFocus.fromParsedTrace(parsedTrace));
164
171
  }
165
172
 
166
173
  static fromInsight(parsedTrace: Trace.TraceModel.ParsedTrace, insight: Trace.Insights.Types.InsightModel):
@@ -224,7 +231,7 @@ export class PerformanceTraceContext extends ConversationContext<AgentFocus> {
224
231
  }
225
232
 
226
233
  if (data.insight) {
227
- return new PerformanceInsightFormatter(data.parsedTrace, data.insight).getSuggestions();
234
+ return new PerformanceInsightFormatter(this.#focus, data.insight).getSuggestions();
228
235
  }
229
236
 
230
237
  const suggestions: ConversationSuggestions =
@@ -247,6 +254,15 @@ export class PerformanceTraceContext extends ConversationContext<AgentFocus> {
247
254
  if (cls && ModelHandlers.LayoutShifts.scoreClassificationForLayoutShift(cls.value) !== GOOD) {
248
255
  suggestions.push({title: 'How can I improve CLS?', jslogContext: 'performance-default'});
249
256
  }
257
+
258
+ // Add up to 3 suggestions from the top failing insights.
259
+ const top3FailingInsightSuggestions =
260
+ Object.values(data.insightSet.model)
261
+ .filter(model => model.state !== 'pass')
262
+ .map(model => new PerformanceInsightFormatter(this.#focus, model).getSuggestions().at(-1))
263
+ .filter(suggestion => !!suggestion)
264
+ .slice(0, 3);
265
+ suggestions.push(...top3FailingInsightSuggestions);
250
266
  }
251
267
 
252
268
  return suggestions;
@@ -263,7 +279,6 @@ const MAX_FUNCTION_RESULT_BYTE_LENGTH = 16384 * 4;
263
279
  export class PerformanceAgent extends AiAgent<AgentFocus> {
264
280
  #formatter: PerformanceTraceFormatter|null = null;
265
281
  #lastInsightForEnhancedQuery: Trace.Insights.Types.InsightModel|undefined;
266
- #eventsSerializer = new Trace.EventsSerializer.EventsSerializer();
267
282
  #hasShownAnalyzeTraceContext = false;
268
283
 
269
284
  /**
@@ -279,8 +294,12 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
279
294
  */
280
295
  #functionCallCacheForFocus = new Map<AgentFocus, Record<string, Host.AidaClient.RequestFact>>();
281
296
 
297
+ #notExternalExtraPreambleFact: Host.AidaClient.RequestFact = {
298
+ text: extraPreambleWhenNotExternal,
299
+ metadata: {source: 'devtools', score: ScorePriority.CRITICAL}
300
+ };
282
301
  #networkDataDescriptionFact: Host.AidaClient.RequestFact = {
283
- text: TraceEventFormatter.networkDataFormatDescription,
302
+ text: PerformanceTraceFormatter.networkDataFormatDescription,
284
303
  metadata: {source: 'devtools', score: ScorePriority.CRITICAL}
285
304
  };
286
305
  #callFrameDataDescriptionFact: Host.AidaClient.RequestFact = {
@@ -290,7 +309,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
290
309
  #traceFacts: Host.AidaClient.RequestFact[] = [];
291
310
 
292
311
  get preamble(): string {
293
- return fullTracePreamble;
312
+ return preamble;
294
313
  }
295
314
 
296
315
  get clientFeature(): Host.AidaClient.ClientFeature {
@@ -313,23 +332,6 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
313
332
  return ConversationType.PERFORMANCE;
314
333
  }
315
334
 
316
- #lookupEvent(key: Trace.Types.File.SerializableKey): Trace.Types.Events.Event|null {
317
- const parsedTrace = this.context?.getItem().data.parsedTrace;
318
- if (!parsedTrace) {
319
- return null;
320
- }
321
-
322
- try {
323
- return this.#eventsSerializer.eventForKey(key, parsedTrace);
324
- } catch (err) {
325
- if (err.toString().includes('Unknown trace event')) {
326
- return null;
327
- }
328
-
329
- throw err;
330
- }
331
- }
332
-
333
335
  async *
334
336
  handleContextDetails(context: ConversationContext<AgentFocus>|null): AsyncGenerator<ContextResponse, void, void> {
335
337
  if (!context) {
@@ -360,21 +362,81 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
360
362
  return response.length > MAX_FUNCTION_RESULT_BYTE_LENGTH;
361
363
  }
362
364
 
363
- override parseTextResponse(response: string): ParsedResponse {
365
+ /**
366
+ * Sometimes the model will output URLs as plaintext; or a markdown link
367
+ * where the link is the actual URL. This function transforms such output
368
+ * to an eventKey link.
369
+ *
370
+ * A simple way to see when this gets utilized is:
371
+ * 1. go to paulirish.com, record a trace
372
+ * 2. say "What performance issues exist with my page?"
373
+ * 3. then say "images"
374
+ *
375
+ * TODO(cjamcl): reduce the reliance on this by making sure all URL references
376
+ * (such as the insight formatters) add the "eventKey" as a suffix, just like all
377
+ * other events.
378
+ */
379
+ #parseForKnownUrls(response: string): string {
380
+ const focus = this.context?.getItem();
381
+ if (!focus) {
382
+ return response;
383
+ }
384
+
385
+ // Regex with two main parts, separated by | (OR):
386
+ // 1. (\[(.*?)\]\((.*?)\)): Captures a full markdown link.
387
+ // - Group 1: The whole link, e.g., "[text](url)"
388
+ // - Group 2: The link text, e.g., "text"
389
+ // - Group 3: The link destination, e.g., "url"
390
+ // 2. (https?:\/\/[^\s<>()]+): Captures a standalone URL.
391
+ // - Group 4: The standalone URL, e.g., "https://google.com"
392
+ const urlRegex = /(\[(.*?)\]\((.*?)\))|(https?:\/\/[^\s<>()]+)/g;
393
+
394
+ return response.replace(urlRegex, (match, markdownLink, linkText, linkDest, standaloneUrlText) => {
395
+ if (markdownLink) {
396
+ if (linkDest.startsWith('#')) {
397
+ return match;
398
+ }
399
+ }
400
+
401
+ const urlText = linkDest ?? standaloneUrlText;
402
+ if (!urlText) {
403
+ return match;
404
+ }
405
+
406
+ const request =
407
+ focus.data.parsedTrace.data.NetworkRequests.byTime.find(request => request.args.data.url === urlText);
408
+ if (!request) {
409
+ return match;
410
+ }
411
+
412
+ const eventKey = focus.eventsSerializer.keyForEvent(request);
413
+ if (!eventKey) {
414
+ return match;
415
+ }
416
+
417
+ return `[${urlText}](#${eventKey})`;
418
+ });
419
+ }
420
+
421
+ #parseMarkdown(response: string): string {
364
422
  /**
365
423
  * Sometimes the LLM responds with code chunks that wrap a text based markdown response.
366
424
  * If this happens, we want to remove those before continuing.
367
425
  * See b/405054694 for more details.
368
426
  */
369
- const trimmed = response.trim();
370
427
  const FIVE_BACKTICKS = '`````';
371
- if (trimmed.startsWith(FIVE_BACKTICKS) && trimmed.endsWith(FIVE_BACKTICKS)) {
372
- // Purposefully use the trimmed text here; we might as well remove any
373
- // newlines that are at the very start or end.
374
- const stripped = trimmed.slice(FIVE_BACKTICKS.length, -FIVE_BACKTICKS.length);
375
- return super.parseTextResponse(stripped);
428
+ if (response.startsWith(FIVE_BACKTICKS) && response.endsWith(FIVE_BACKTICKS)) {
429
+ return response.slice(FIVE_BACKTICKS.length, -FIVE_BACKTICKS.length);
376
430
  }
377
- return super.parseTextResponse(response);
431
+
432
+ return response;
433
+ }
434
+
435
+ override parseTextResponse(response: string): ParsedResponse {
436
+ const parsedResponse = super.parseTextResponse(response);
437
+ parsedResponse.answer = this.#parseForKnownUrls(parsedResponse.answer);
438
+ parsedResponse.answer = this.#parseMarkdown(parsedResponse.answer);
439
+ return parsedResponse;
378
440
  }
379
441
 
380
442
  override async enhanceQuery(query: string, context: PerformanceTraceContext|null): Promise<string> {
@@ -426,15 +488,15 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
426
488
  }
427
489
 
428
490
  override async * run(initialQuery: string, options: {
429
- selected: ConversationContext<AgentFocus>|null,
491
+ selected: PerformanceTraceContext|null,
430
492
  signal?: AbortSignal,
431
493
  }): AsyncGenerator<ResponseData, void, void> {
432
494
  const focus = options.selected?.getItem();
433
495
 
434
496
  // Clear any previous facts in case the user changed the active context.
435
497
  this.clearFacts();
436
- if (focus) {
437
- this.#addFacts(focus);
498
+ if (options.selected && focus) {
499
+ this.#addFacts(options.selected);
438
500
  }
439
501
 
440
502
  return yield* super.run(initialQuery, options);
@@ -518,12 +580,18 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
518
580
  });
519
581
  }
520
582
 
521
- #addFacts(focus: AgentFocus): void {
583
+ #addFacts(context: PerformanceTraceContext): void {
584
+ const focus = context.getItem();
585
+
586
+ if (!context.external) {
587
+ this.addFact(this.#notExternalExtraPreambleFact);
588
+ }
589
+
522
590
  this.addFact(this.#callFrameDataDescriptionFact);
523
591
  this.addFact(this.#networkDataDescriptionFact);
524
592
 
525
593
  if (!this.#traceFacts.length) {
526
- this.#formatter = new PerformanceTraceFormatter(focus, this.#eventsSerializer);
594
+ this.#formatter = new PerformanceTraceFormatter(focus);
527
595
  this.#createFactForTraceSummary();
528
596
  this.#createFactForCriticalRequests();
529
597
  this.#createFactForMainThreadBottomUpSummary();
@@ -585,7 +653,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
585
653
  return {error: 'No insight available'};
586
654
  }
587
655
 
588
- const details = new PerformanceInsightFormatter(parsedTrace, insight).formatInsight();
656
+ const details = new PerformanceInsightFormatter(focus, insight).formatInsight();
589
657
 
590
658
  const key = `getInsightDetails('${params.insightName}')`;
591
659
  this.#cacheFunctionResult(focus, key, details);
@@ -613,7 +681,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
613
681
  },
614
682
  handler: async params => {
615
683
  debugLog('Function call: getEventByKey', params);
616
- const event = this.#lookupEvent(params.eventKey as Trace.Types.File.SerializableKey);
684
+ const event = focus.lookupEvent(params.eventKey as Trace.Types.File.SerializableKey);
617
685
  if (!event) {
618
686
  return {error: 'Invalid eventKey'};
619
687
  }
@@ -772,7 +840,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
772
840
  },
773
841
  },
774
842
  displayInfoFromArgs: args => {
775
- return {title: lockedString('Looking at call tree…'), action: `getDetailedCallTree(${args.eventKey})`};
843
+ return {title: lockedString('Looking at call tree…'), action: `getDetailedCallTree('${args.eventKey}')`};
776
844
  },
777
845
  handler: async args => {
778
846
  debugLog('Function call: getDetailedCallTree');
@@ -781,7 +849,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
781
849
  throw new Error('missing formatter');
782
850
  }
783
851
 
784
- const event = this.#lookupEvent(args.eventKey as Trace.Types.File.SerializableKey);
852
+ const event = focus.lookupEvent(args.eventKey as Trace.Types.File.SerializableKey);
785
853
  if (!event) {
786
854
  return {error: 'Invalid eventKey'};
787
855
  }
@@ -816,7 +884,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
816
884
  },
817
885
  },
818
886
  displayInfoFromArgs: args => {
819
- return {title: lockedString('Looking at resource content…'), action: `getResourceContent(${args.url})`};
887
+ return {title: lockedString('Looking at resource content…'), action: `getResourceContent('${args.url}')`};
820
888
  },
821
889
  handler: async args => {
822
890
  debugLog('Function call: getResourceContent');
@@ -829,14 +897,14 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
829
897
  }
830
898
  }
831
899
 
832
- const content = resource.content;
833
- if (!content) {
834
- return {error: 'Resource has no content'};
900
+ const content = await resource.requestContentData();
901
+ if ('error' in content) {
902
+ return {error: `Could not get resource content: ${content.error}`};
835
903
  }
836
904
 
837
905
  const key = `getResourceContent(${args.url})`;
838
- this.#cacheFunctionResult(focus, key, content);
839
- return {result: {content}};
906
+ this.#cacheFunctionResult(focus, key, content.text);
907
+ return {result: {content: content.text}};
840
908
  },
841
909
 
842
910
  });
@@ -863,7 +931,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
863
931
  },
864
932
  handler: async params => {
865
933
  debugLog('Function call: selectEventByKey', params);
866
- const event = this.#lookupEvent(params.eventKey as Trace.Types.File.SerializableKey);
934
+ const event = focus.lookupEvent(params.eventKey as Trace.Types.File.SerializableKey);
867
935
  if (!event) {
868
936
  return {error: 'Invalid eventKey'};
869
937
  }
@@ -21,7 +21,6 @@ import {
21
21
  type ConversationSuggestions,
22
22
  type FunctionCallHandlerResult,
23
23
  MultimodalInputType,
24
- type ParsedResponse,
25
24
  type RequestOptions,
26
25
  ResponseType,
27
26
  } from './AiAgent.js';
@@ -265,36 +264,6 @@ export class StylingAgent extends AiAgent<SDK.DOMModel.DOMNode> {
265
264
  override preambleFeatures(): string[] {
266
265
  return ['function_calling'];
267
266
  }
268
- override parseTextResponse(text: string): ParsedResponse {
269
- // We're returning an empty answer to denote the erroneous case.
270
- if (!text.trim()) {
271
- return {answer: ''};
272
- }
273
-
274
- const lines = text.split('\n');
275
- const answerLines: string[] = [];
276
- let suggestions: [string, ...string[]]|undefined;
277
-
278
- for (const line of lines) {
279
- const trimmed = line.trim();
280
- if (trimmed.startsWith('SUGGESTIONS:')) {
281
- try {
282
- // TODO: Do basic validation this is an array with strings
283
- suggestions = JSON.parse(trimmed.substring('SUGGESTIONS:'.length).trim());
284
- } catch {
285
- }
286
- } else {
287
- answerLines.push(line);
288
- }
289
- }
290
-
291
- return {
292
- // If we could not parse the parts, consider the response to be an
293
- // answer.
294
- answer: answerLines.join('\n'),
295
- suggestions,
296
- };
297
- }
298
267
 
299
268
  #execJs: typeof executeJsCode;
300
269