chrome-devtools-frontend 1.0.1515988 → 1.0.1518653

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 (122) hide show
  1. package/docs/checklist/README.md +2 -2
  2. package/docs/checklist/javascript.md +1 -1
  3. package/docs/contributing/README.md +1 -1
  4. package/docs/contributing/settings-experiments-features.md +9 -8
  5. package/docs/cookbook/devtools_on_devtools.md +2 -2
  6. package/docs/cookbook/localization.md +10 -10
  7. package/docs/devtools-protocol.md +9 -8
  8. package/docs/ecosystem/automatic_workspace_folders.md +3 -3
  9. package/docs/get_the_code.md +0 -2
  10. package/docs/styleguide/ux/components.md +166 -85
  11. package/docs/styleguide/ux/numbers.md +3 -4
  12. package/front_end/core/common/README.md +13 -12
  13. package/front_end/core/host/GdpClient.ts +16 -1
  14. package/front_end/core/host/UserMetrics.ts +8 -2
  15. package/front_end/core/root/Runtime.ts +13 -0
  16. package/front_end/core/sdk/CSSMatchedStyles.ts +5 -1
  17. package/front_end/entrypoints/main/MainImpl.ts +6 -3
  18. package/front_end/generated/InspectorBackendCommands.js +10 -7
  19. package/front_end/generated/SupportedCSSProperties.js +21 -7
  20. package/front_end/generated/protocol-mapping.d.ts +16 -1
  21. package/front_end/generated/protocol-proxy-api.d.ts +13 -1
  22. package/front_end/generated/protocol.ts +95 -0
  23. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +170 -54
  24. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.snapshot.txt +14 -181
  25. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.ts +13 -315
  26. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.snapshot.txt +224 -50
  27. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.ts +310 -11
  28. package/front_end/models/ai_assistance/performance/AIContext.ts +15 -2
  29. package/front_end/models/ai_code_completion/AiCodeCompletion.ts +41 -19
  30. package/front_end/models/badges/Badge.ts +8 -3
  31. package/front_end/models/badges/CodeWhispererBadge.ts +2 -4
  32. package/front_end/models/badges/StarterBadge.ts +2 -2
  33. package/front_end/models/badges/UserBadges.ts +59 -6
  34. package/front_end/models/formatter/FormatterWorkerPool.ts +3 -3
  35. package/front_end/models/javascript_metadata/NativeFunctions.js +1 -1
  36. package/front_end/models/trace/README.md +28 -1
  37. package/front_end/models/trace/handlers/UserTimingsHandler.ts +1 -1
  38. package/front_end/models/trace/helpers/Trace.ts +99 -43
  39. package/front_end/models/trace/types/TraceEvents.ts +9 -0
  40. package/front_end/panels/accessibility/ARIAAttributesView.ts +113 -191
  41. package/front_end/panels/accessibility/AccessibilityNodeView.ts +9 -9
  42. package/front_end/panels/accessibility/AccessibilitySubPane.ts +6 -4
  43. package/front_end/panels/accessibility/accessibilityProperties.css +2 -0
  44. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +16 -2
  45. package/front_end/panels/ai_assistance/components/ChatView.ts +9 -10
  46. package/front_end/panels/ai_assistance/components/PerformanceAgentMarkdownRenderer.ts +42 -0
  47. package/front_end/panels/common/AiCodeCompletionDisclaimer.ts +32 -9
  48. package/front_end/panels/common/AiCodeCompletionSummaryToolbar.ts +7 -1
  49. package/front_end/panels/common/BadgeNotification.ts +67 -15
  50. package/front_end/panels/common/GdpSignUpDialog.ts +18 -9
  51. package/front_end/panels/console/ConsolePrompt.ts +1 -1
  52. package/front_end/panels/console/ConsoleView.ts +6 -2
  53. package/front_end/panels/elements/ComputedStyleWidget.ts +1 -2
  54. package/front_end/panels/elements/ElementsPanel.ts +4 -0
  55. package/front_end/panels/elements/ElementsTreeElement.ts +18 -0
  56. package/front_end/panels/elements/ElementsTreeOutline.ts +13 -0
  57. package/front_end/panels/elements/LayoutPane.ts +1 -1
  58. package/front_end/panels/elements/StylePropertyTreeElement.ts +21 -6
  59. package/front_end/panels/media/TickingFlameChart.ts +1 -1
  60. package/front_end/panels/network/NetworkLogView.ts +5 -1
  61. package/front_end/panels/profiler/HeapSnapshotView.ts +34 -19
  62. package/front_end/panels/search/SearchResultsPane.ts +126 -145
  63. package/front_end/panels/search/SearchView.ts +43 -59
  64. package/front_end/panels/settings/components/SyncSection.ts +16 -8
  65. package/front_end/panels/sources/AiCodeCompletionPlugin.ts +6 -1
  66. package/front_end/panels/sources/OutlineQuickOpen.ts +3 -1
  67. package/front_end/panels/sources/SourcesPanel.ts +3 -0
  68. package/front_end/panels/timeline/AppenderUtils.ts +2 -2
  69. package/front_end/panels/timeline/ExtensionTrackAppender.ts +13 -4
  70. package/front_end/panels/timeline/GPUTrackAppender.ts +2 -1
  71. package/front_end/panels/timeline/InteractionsTrackAppender.ts +5 -1
  72. package/front_end/panels/timeline/LayoutShiftsTrackAppender.ts +2 -1
  73. package/front_end/panels/timeline/ThreadAppender.ts +12 -3
  74. package/front_end/panels/timeline/TimelineFlameChartDataProvider.ts +9 -4
  75. package/front_end/panels/timeline/TimelinePanel.ts +3 -2
  76. package/front_end/panels/timeline/TimelineUIUtils.ts +18 -12
  77. package/front_end/panels/timeline/TimingsTrackAppender.ts +6 -1
  78. package/front_end/panels/timeline/components/CPUThrottlingSelector.ts +95 -82
  79. package/front_end/panels/timeline/components/LiveMetricsView.ts +2 -2
  80. package/front_end/panels/timeline/components/cpuThrottlingSelector.css +17 -15
  81. package/front_end/panels/timeline/components/insights/BaseInsightComponent.ts +3 -0
  82. package/front_end/third_party/chromium/README.chromium +1 -1
  83. package/front_end/third_party/codemirror.next/chunk/codemirror.js +1 -1
  84. package/front_end/third_party/codemirror.next/chunk/codemirror.js.map +1 -1
  85. package/front_end/third_party/codemirror.next/codemirror.next.d.ts +6 -9
  86. package/front_end/third_party/codemirror.next/package.json +2 -1
  87. package/front_end/third_party/diff/README.chromium +1 -0
  88. package/front_end/third_party/puppeteer/README.chromium +2 -2
  89. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/core/Realm.d.ts +2 -2
  90. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/Accessibility.js +0 -20
  91. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/Accessibility.js.map +1 -1
  92. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/version.d.ts +1 -1
  93. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/version.js +1 -1
  94. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/injected/injected.d.ts +1 -1
  95. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.d.ts +1 -1
  96. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.js +1 -1
  97. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.js.map +1 -1
  98. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/Mutex.d.ts +2 -2
  99. package/front_end/third_party/puppeteer/package/lib/es5-iife/puppeteer-core-browser.js +2 -23
  100. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/Accessibility.js +0 -20
  101. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/Accessibility.js.map +1 -1
  102. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/version.d.ts +1 -1
  103. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/version.js +1 -1
  104. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.d.ts +1 -1
  105. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.js +1 -1
  106. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.js.map +1 -1
  107. package/front_end/third_party/puppeteer/package/package.json +1 -1
  108. package/front_end/third_party/puppeteer/package/src/cdp/Accessibility.ts +1 -21
  109. package/front_end/third_party/puppeteer/package/src/generated/version.ts +1 -1
  110. package/front_end/third_party/puppeteer/package/src/revisions.ts +1 -1
  111. package/front_end/ui/components/text_editor/config.ts +36 -8
  112. package/front_end/ui/components/tooltips/Tooltip.ts +71 -34
  113. package/front_end/ui/legacy/README.md +33 -24
  114. package/front_end/ui/legacy/SearchableView.ts +19 -26
  115. package/front_end/ui/legacy/TextPrompt.ts +166 -1
  116. package/front_end/ui/legacy/Treeoutline.ts +16 -2
  117. package/front_end/ui/legacy/UIUtils.ts +15 -2
  118. package/front_end/ui/legacy/XElement.ts +0 -43
  119. package/front_end/ui/legacy/components/perf_ui/FlameChart.ts +20 -4
  120. package/front_end/ui/visual_logging/KnownContextValues.ts +24 -6
  121. package/front_end/ui/visual_logging/README.md +43 -27
  122. 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 {
@@ -15,7 +15,6 @@ import * as Trace from '../../trace/trace.js';
15
15
  import {ConversationType} from '../AiHistoryStorage.js';
16
16
  import {
17
17
  PerformanceInsightFormatter,
18
- TraceEventFormatter,
19
18
  } from '../data_formatters/PerformanceInsightFormatter.js';
20
19
  import {PerformanceTraceFormatter} from '../data_formatters/PerformanceTraceFormatter.js';
21
20
  import {debugLog} from '../debug.js';
@@ -62,7 +61,7 @@ const lockedString = i18n.i18n.lockedString;
62
61
  *
63
62
  * Check token length in https://aistudio.google.com/
64
63
  */
65
- const fullTracePreamble = `You are an assistant, expert in web performance and highly skilled with Chrome DevTools.
64
+ const preamble = `You are an assistant, expert in web performance and highly skilled with Chrome DevTools.
66
65
 
67
66
  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
67
 
@@ -129,6 +128,16 @@ Adhere to the following critical requirements:
129
128
  - Do not provide answers on non-web-development topics, such as legal, financial, medical, or personal advice.
130
129
  `;
131
130
 
131
+ const extraPreambleWhenNotExternal = `Additional notes:
132
+
133
+ When referring to a trace event that has a corresponding \`eventKey\`, annotate your output using markdown link syntax. For example:
134
+ - When referring to an event that is a long task: [Long task](#r-123)
135
+ - When referring to a URL for which you know the eventKey of: [https://www.example.com](#s-1827)
136
+ - Never show the eventKey (like "eventKey: s-1852"); instead, use a markdown link as described above.
137
+
138
+ 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.
139
+ `;
140
+
132
141
  const callFrameDataFormatDescription = `Each call frame is presented in the following format:
133
142
 
134
143
  'id;name;duration;selfTime;urlIndex;childRange;[S]'
@@ -159,8 +168,8 @@ enum ScorePriority {
159
168
  }
160
169
 
161
170
  export class PerformanceTraceContext extends ConversationContext<AgentFocus> {
162
- static full(parsedTrace: Trace.TraceModel.ParsedTrace): PerformanceTraceContext {
163
- return new PerformanceTraceContext(AgentFocus.full(parsedTrace));
171
+ static fromParsedTrace(parsedTrace: Trace.TraceModel.ParsedTrace): PerformanceTraceContext {
172
+ return new PerformanceTraceContext(AgentFocus.fromParsedTrace(parsedTrace));
164
173
  }
165
174
 
166
175
  static fromInsight(parsedTrace: Trace.TraceModel.ParsedTrace, insight: Trace.Insights.Types.InsightModel):
@@ -224,7 +233,7 @@ export class PerformanceTraceContext extends ConversationContext<AgentFocus> {
224
233
  }
225
234
 
226
235
  if (data.insight) {
227
- return new PerformanceInsightFormatter(data.parsedTrace, data.insight).getSuggestions();
236
+ return new PerformanceInsightFormatter(this.#focus, data.insight).getSuggestions();
228
237
  }
229
238
 
230
239
  const suggestions: ConversationSuggestions =
@@ -247,6 +256,15 @@ export class PerformanceTraceContext extends ConversationContext<AgentFocus> {
247
256
  if (cls && ModelHandlers.LayoutShifts.scoreClassificationForLayoutShift(cls.value) !== GOOD) {
248
257
  suggestions.push({title: 'How can I improve CLS?', jslogContext: 'performance-default'});
249
258
  }
259
+
260
+ // Add up to 3 suggestions from the top failing insights.
261
+ const top3FailingInsightSuggestions =
262
+ Object.values(data.insightSet.model)
263
+ .filter(model => model.state !== 'pass')
264
+ .map(model => new PerformanceInsightFormatter(this.#focus, model).getSuggestions().at(-1))
265
+ .filter(suggestion => !!suggestion)
266
+ .slice(0, 3);
267
+ suggestions.push(...top3FailingInsightSuggestions);
250
268
  }
251
269
 
252
270
  return suggestions;
@@ -263,8 +281,7 @@ const MAX_FUNCTION_RESULT_BYTE_LENGTH = 16384 * 4;
263
281
  export class PerformanceAgent extends AiAgent<AgentFocus> {
264
282
  #formatter: PerformanceTraceFormatter|null = null;
265
283
  #lastInsightForEnhancedQuery: Trace.Insights.Types.InsightModel|undefined;
266
- #eventsSerializer = new Trace.EventsSerializer.EventsSerializer();
267
- #lastFocusHandledForContextDetails: AgentFocus|null = null;
284
+ #hasShownAnalyzeTraceContext = false;
268
285
 
269
286
  /**
270
287
  * Cache of all function calls made by the agent. This allows us to include (as a
@@ -279,8 +296,12 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
279
296
  */
280
297
  #functionCallCacheForFocus = new Map<AgentFocus, Record<string, Host.AidaClient.RequestFact>>();
281
298
 
299
+ #notExternalExtraPreambleFact: Host.AidaClient.RequestFact = {
300
+ text: extraPreambleWhenNotExternal,
301
+ metadata: {source: 'devtools', score: ScorePriority.CRITICAL}
302
+ };
282
303
  #networkDataDescriptionFact: Host.AidaClient.RequestFact = {
283
- text: TraceEventFormatter.networkDataFormatDescription,
304
+ text: PerformanceTraceFormatter.networkDataFormatDescription,
284
305
  metadata: {source: 'devtools', score: ScorePriority.CRITICAL}
285
306
  };
286
307
  #callFrameDataDescriptionFact: Host.AidaClient.RequestFact = {
@@ -290,7 +311,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
290
311
  #traceFacts: Host.AidaClient.RequestFact[] = [];
291
312
 
292
313
  get preamble(): string {
293
- return fullTracePreamble;
314
+ return preamble;
294
315
  }
295
316
 
296
317
  get clientFeature(): Host.AidaClient.ClientFeature {
@@ -313,36 +334,16 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
313
334
  return ConversationType.PERFORMANCE;
314
335
  }
315
336
 
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
337
  async *
334
338
  handleContextDetails(context: ConversationContext<AgentFocus>|null): AsyncGenerator<ContextResponse, void, void> {
335
339
  if (!context) {
336
340
  return;
337
341
  }
338
342
 
339
- const focus = context.getItem();
340
- if (this.#lastFocusHandledForContextDetails === focus) {
343
+ if (this.#hasShownAnalyzeTraceContext) {
341
344
  return;
342
345
  }
343
346
 
344
- this.#lastFocusHandledForContextDetails = focus;
345
-
346
347
  yield {
347
348
  type: ResponseType.CONTEXT,
348
349
  title: lockedString(UIStringsNotTranslated.analyzingTrace),
@@ -353,6 +354,8 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
353
354
  },
354
355
  ],
355
356
  };
357
+
358
+ this.#hasShownAnalyzeTraceContext = true;
356
359
  }
357
360
 
358
361
  #callTreeContextSet = new WeakSet();
@@ -361,21 +364,128 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
361
364
  return response.length > MAX_FUNCTION_RESULT_BYTE_LENGTH;
362
365
  }
363
366
 
364
- override parseTextResponse(response: string): ParsedResponse {
367
+ /**
368
+ * Sometimes the model will output URLs as plaintext; or a markdown link
369
+ * where the link is the actual URL. This function transforms such output
370
+ * to an eventKey link.
371
+ *
372
+ * A simple way to see when this gets utilized is:
373
+ * 1. go to paulirish.com, record a trace
374
+ * 2. say "What performance issues exist with my page?"
375
+ * 3. then say "images"
376
+ *
377
+ * TODO(cjamcl): reduce the reliance on this by making sure all URL references
378
+ * (such as the insight formatters) add the "eventKey" as a suffix, just like all
379
+ * other events.
380
+ */
381
+ #parseForKnownUrls(response: string): string {
382
+ const focus = this.context?.getItem();
383
+ if (!focus) {
384
+ return response;
385
+ }
386
+
387
+ // Regex with two main parts, separated by | (OR):
388
+ // 1. (\[(.*?)\]\((.*?)\)): Captures a full markdown link.
389
+ // - Group 1: The whole link, e.g., "[text](url)"
390
+ // - Group 2: The link text, e.g., "text"
391
+ // - Group 3: The link destination, e.g., "url"
392
+ // 2. (https?:\/\/[^\s<>()]+): Captures a standalone URL.
393
+ // - Group 4: The standalone URL, e.g., "https://google.com"
394
+ const urlRegex = /(\[(.*?)\]\((.*?)\))|(https?:\/\/[^\s<>()]+)/g;
395
+
396
+ return response.replace(urlRegex, (match, markdownLink, linkText, linkDest, standaloneUrlText) => {
397
+ if (markdownLink) {
398
+ if (linkDest.startsWith('#')) {
399
+ return match;
400
+ }
401
+ }
402
+
403
+ const urlText = linkDest ?? standaloneUrlText;
404
+ if (!urlText) {
405
+ return match;
406
+ }
407
+
408
+ const request =
409
+ focus.data.parsedTrace.data.NetworkRequests.byTime.find(request => request.args.data.url === urlText);
410
+ if (!request) {
411
+ return match;
412
+ }
413
+
414
+ const eventKey = focus.eventsSerializer.keyForEvent(request);
415
+ if (!eventKey) {
416
+ return match;
417
+ }
418
+
419
+ return `[${urlText}](#${eventKey})`;
420
+ });
421
+ }
422
+
423
+ #parseSuggestions(text: string): ParsedResponse {
424
+ if (!text) {
425
+ return {answer: ''};
426
+ }
427
+
428
+ const lines = text.split('\n');
429
+ const answerLines: string[] = [];
430
+ let suggestions: [string, ...string[]]|undefined;
431
+
432
+ for (const line of lines) {
433
+ const trimmed = line.trim();
434
+ if (trimmed.startsWith('SUGGESTIONS:')) {
435
+ try {
436
+ // TODO: Do basic validation this is an array with strings
437
+ suggestions = JSON.parse(trimmed.substring('SUGGESTIONS:'.length).trim());
438
+ } catch {
439
+ }
440
+ } else {
441
+ answerLines.push(line);
442
+ }
443
+ }
444
+
445
+ // Sometimes the model fails to put the SUGGESTIONS text on its own line. Handle
446
+ // the case where the suggestions are part of the last line of the answer.
447
+ if (!suggestions && answerLines.at(-1)?.includes('SUGGESTIONS:')) {
448
+ const [answer, suggestionsText] = answerLines[answerLines.length - 1].split('SUGGESTIONS:', 2);
449
+ try {
450
+ // TODO: Do basic validation this is an array with strings
451
+ suggestions = JSON.parse(suggestionsText.trim().substring('SUGGESTIONS:'.length).trim());
452
+ } catch {
453
+ }
454
+ answerLines[answerLines.length - 1] = answer;
455
+ }
456
+
457
+ const response: ParsedResponse = {
458
+ // If we could not parse the parts, consider the response to be an
459
+ // answer.
460
+ answer: answerLines.join('\n'),
461
+ };
462
+
463
+ if (suggestions) {
464
+ response.suggestions = suggestions;
465
+ }
466
+
467
+ return response;
468
+ }
469
+
470
+ #parseMarkdown(response: string): string {
365
471
  /**
366
472
  * Sometimes the LLM responds with code chunks that wrap a text based markdown response.
367
473
  * If this happens, we want to remove those before continuing.
368
474
  * See b/405054694 for more details.
369
475
  */
370
- const trimmed = response.trim();
371
476
  const FIVE_BACKTICKS = '`````';
372
- if (trimmed.startsWith(FIVE_BACKTICKS) && trimmed.endsWith(FIVE_BACKTICKS)) {
373
- // Purposefully use the trimmed text here; we might as well remove any
374
- // newlines that are at the very start or end.
375
- const stripped = trimmed.slice(FIVE_BACKTICKS.length, -FIVE_BACKTICKS.length);
376
- return super.parseTextResponse(stripped);
477
+ if (response.startsWith(FIVE_BACKTICKS) && response.endsWith(FIVE_BACKTICKS)) {
478
+ return response.slice(FIVE_BACKTICKS.length, -FIVE_BACKTICKS.length);
377
479
  }
378
- return super.parseTextResponse(response);
480
+
481
+ return response;
482
+ }
483
+
484
+ override parseTextResponse(response: string): ParsedResponse {
485
+ response = response.trim();
486
+ response = this.#parseForKnownUrls(response);
487
+ response = this.#parseMarkdown(response);
488
+ return this.#parseSuggestions(response);
379
489
  }
380
490
 
381
491
  override async enhanceQuery(query: string, context: PerformanceTraceContext|null): Promise<string> {
@@ -427,15 +537,15 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
427
537
  }
428
538
 
429
539
  override async * run(initialQuery: string, options: {
430
- selected: ConversationContext<AgentFocus>|null,
540
+ selected: PerformanceTraceContext|null,
431
541
  signal?: AbortSignal,
432
542
  }): AsyncGenerator<ResponseData, void, void> {
433
543
  const focus = options.selected?.getItem();
434
544
 
435
545
  // Clear any previous facts in case the user changed the active context.
436
546
  this.clearFacts();
437
- if (focus) {
438
- this.#addFacts(focus);
547
+ if (options.selected && focus) {
548
+ this.#addFacts(options.selected);
439
549
  }
440
550
 
441
551
  return yield* super.run(initialQuery, options);
@@ -519,12 +629,18 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
519
629
  });
520
630
  }
521
631
 
522
- #addFacts(focus: AgentFocus): void {
632
+ #addFacts(context: PerformanceTraceContext): void {
633
+ const focus = context.getItem();
634
+
635
+ if (!context.external) {
636
+ this.addFact(this.#notExternalExtraPreambleFact);
637
+ }
638
+
523
639
  this.addFact(this.#callFrameDataDescriptionFact);
524
640
  this.addFact(this.#networkDataDescriptionFact);
525
641
 
526
642
  if (!this.#traceFacts.length) {
527
- this.#formatter = new PerformanceTraceFormatter(focus, this.#eventsSerializer);
643
+ this.#formatter = new PerformanceTraceFormatter(focus);
528
644
  this.#createFactForTraceSummary();
529
645
  this.#createFactForCriticalRequests();
530
646
  this.#createFactForMainThreadBottomUpSummary();
@@ -586,7 +702,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
586
702
  return {error: 'No insight available'};
587
703
  }
588
704
 
589
- const details = new PerformanceInsightFormatter(parsedTrace, insight).formatInsight();
705
+ const details = new PerformanceInsightFormatter(focus, insight).formatInsight();
590
706
 
591
707
  const key = `getInsightDetails('${params.insightName}')`;
592
708
  this.#cacheFunctionResult(focus, key, details);
@@ -614,7 +730,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
614
730
  },
615
731
  handler: async params => {
616
732
  debugLog('Function call: getEventByKey', params);
617
- const event = this.#lookupEvent(params.eventKey as Trace.Types.File.SerializableKey);
733
+ const event = focus.lookupEvent(params.eventKey as Trace.Types.File.SerializableKey);
618
734
  if (!event) {
619
735
  return {error: 'Invalid eventKey'};
620
736
  }
@@ -773,7 +889,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
773
889
  },
774
890
  },
775
891
  displayInfoFromArgs: args => {
776
- return {title: lockedString('Looking at call tree…'), action: `getDetailedCallTree(${args.eventKey})`};
892
+ return {title: lockedString('Looking at call tree…'), action: `getDetailedCallTree('${args.eventKey}')`};
777
893
  },
778
894
  handler: async args => {
779
895
  debugLog('Function call: getDetailedCallTree');
@@ -782,7 +898,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
782
898
  throw new Error('missing formatter');
783
899
  }
784
900
 
785
- const event = this.#lookupEvent(args.eventKey as Trace.Types.File.SerializableKey);
901
+ const event = focus.lookupEvent(args.eventKey as Trace.Types.File.SerializableKey);
786
902
  if (!event) {
787
903
  return {error: 'Invalid eventKey'};
788
904
  }
@@ -817,7 +933,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
817
933
  },
818
934
  },
819
935
  displayInfoFromArgs: args => {
820
- return {title: lockedString('Looking at resource content…'), action: `getResourceContent(${args.url})`};
936
+ return {title: lockedString('Looking at resource content…'), action: `getResourceContent('${args.url}')`};
821
937
  },
822
938
  handler: async args => {
823
939
  debugLog('Function call: getResourceContent');
@@ -830,14 +946,14 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
830
946
  }
831
947
  }
832
948
 
833
- const content = resource.content;
834
- if (!content) {
835
- return {error: 'Resource has no content'};
949
+ const content = await resource.requestContentData();
950
+ if ('error' in content) {
951
+ return {error: `Could not get resource content: ${content.error}`};
836
952
  }
837
953
 
838
954
  const key = `getResourceContent(${args.url})`;
839
- this.#cacheFunctionResult(focus, key, content);
840
- return {result: {content}};
955
+ this.#cacheFunctionResult(focus, key, content.text);
956
+ return {result: {content: content.text}};
841
957
  },
842
958
 
843
959
  });
@@ -864,7 +980,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
864
980
  },
865
981
  handler: async params => {
866
982
  debugLog('Function call: selectEventByKey', params);
867
- const event = this.#lookupEvent(params.eventKey as Trace.Types.File.SerializableKey);
983
+ const event = focus.lookupEvent(params.eventKey as Trace.Types.File.SerializableKey);
868
984
  if (!event) {
869
985
  return {error: 'Invalid eventKey'};
870
986
  }