chrome-devtools-frontend 1.0.1516909 → 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 (94) 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 +4 -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 +166 -49
  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 +17 -11
  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 +21 -3
  34. package/front_end/models/javascript_metadata/NativeFunctions.js +1 -1
  35. package/front_end/models/trace/README.md +28 -1
  36. package/front_end/models/trace/handlers/UserTimingsHandler.ts +1 -1
  37. package/front_end/models/trace/helpers/Trace.ts +99 -43
  38. package/front_end/models/trace/types/TraceEvents.ts +9 -0
  39. package/front_end/panels/accessibility/ARIAAttributesView.ts +113 -191
  40. package/front_end/panels/accessibility/AccessibilityNodeView.ts +9 -9
  41. package/front_end/panels/accessibility/AccessibilitySubPane.ts +6 -4
  42. package/front_end/panels/accessibility/accessibilityProperties.css +2 -0
  43. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +16 -2
  44. package/front_end/panels/ai_assistance/components/ChatView.ts +9 -10
  45. package/front_end/panels/ai_assistance/components/PerformanceAgentMarkdownRenderer.ts +42 -0
  46. package/front_end/panels/common/AiCodeCompletionDisclaimer.ts +32 -9
  47. package/front_end/panels/common/AiCodeCompletionSummaryToolbar.ts +7 -1
  48. package/front_end/panels/common/BadgeNotification.ts +21 -5
  49. package/front_end/panels/common/GdpSignUpDialog.ts +18 -9
  50. package/front_end/panels/console/ConsolePrompt.ts +1 -1
  51. package/front_end/panels/console/ConsoleView.ts +6 -2
  52. package/front_end/panels/elements/ElementsPanel.ts +4 -0
  53. package/front_end/panels/elements/ElementsTreeElement.ts +18 -0
  54. package/front_end/panels/elements/ElementsTreeOutline.ts +13 -0
  55. package/front_end/panels/elements/StylePropertyTreeElement.ts +21 -6
  56. package/front_end/panels/media/TickingFlameChart.ts +1 -1
  57. package/front_end/panels/profiler/HeapSnapshotView.ts +34 -19
  58. package/front_end/panels/search/SearchResultsPane.ts +124 -128
  59. package/front_end/panels/search/SearchView.ts +24 -17
  60. package/front_end/panels/settings/components/SyncSection.ts +16 -8
  61. package/front_end/panels/sources/AiCodeCompletionPlugin.ts +6 -1
  62. package/front_end/panels/sources/SourcesPanel.ts +3 -0
  63. package/front_end/panels/timeline/AppenderUtils.ts +2 -2
  64. package/front_end/panels/timeline/ExtensionTrackAppender.ts +13 -4
  65. package/front_end/panels/timeline/GPUTrackAppender.ts +2 -1
  66. package/front_end/panels/timeline/InteractionsTrackAppender.ts +5 -1
  67. package/front_end/panels/timeline/LayoutShiftsTrackAppender.ts +2 -1
  68. package/front_end/panels/timeline/ThreadAppender.ts +12 -3
  69. package/front_end/panels/timeline/TimelineFlameChartDataProvider.ts +9 -4
  70. package/front_end/panels/timeline/TimelinePanel.ts +3 -2
  71. package/front_end/panels/timeline/TimelineUIUtils.ts +5 -4
  72. package/front_end/panels/timeline/TimingsTrackAppender.ts +6 -1
  73. package/front_end/panels/timeline/components/CPUThrottlingSelector.ts +95 -82
  74. package/front_end/panels/timeline/components/LiveMetricsView.ts +2 -2
  75. package/front_end/panels/timeline/components/cpuThrottlingSelector.css +17 -15
  76. package/front_end/panels/timeline/components/insights/BaseInsightComponent.ts +3 -0
  77. package/front_end/third_party/chromium/README.chromium +1 -1
  78. package/front_end/third_party/codemirror.next/chunk/codemirror.js +1 -1
  79. package/front_end/third_party/codemirror.next/chunk/codemirror.js.map +1 -1
  80. package/front_end/third_party/codemirror.next/codemirror.next.d.ts +6 -9
  81. package/front_end/third_party/codemirror.next/package.json +2 -1
  82. package/front_end/third_party/diff/README.chromium +1 -0
  83. package/front_end/ui/components/text_editor/config.ts +6 -7
  84. package/front_end/ui/components/tooltips/Tooltip.ts +70 -31
  85. package/front_end/ui/legacy/README.md +33 -24
  86. package/front_end/ui/legacy/SearchableView.ts +19 -26
  87. package/front_end/ui/legacy/TextPrompt.ts +166 -1
  88. package/front_end/ui/legacy/Treeoutline.ts +16 -2
  89. package/front_end/ui/legacy/UIUtils.ts +15 -2
  90. package/front_end/ui/legacy/XElement.ts +0 -43
  91. package/front_end/ui/legacy/components/perf_ui/FlameChart.ts +20 -4
  92. package/front_end/ui/visual_logging/KnownContextValues.ts +19 -6
  93. package/front_end/ui/visual_logging/README.md +43 -27
  94. 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,7 +281,6 @@ 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
284
  #hasShownAnalyzeTraceContext = false;
268
285
 
269
286
  /**
@@ -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,23 +334,6 @@ 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) {
@@ -360,21 +364,128 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
360
364
  return response.length > MAX_FUNCTION_RESULT_BYTE_LENGTH;
361
365
  }
362
366
 
363
- 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 {
364
471
  /**
365
472
  * Sometimes the LLM responds with code chunks that wrap a text based markdown response.
366
473
  * If this happens, we want to remove those before continuing.
367
474
  * See b/405054694 for more details.
368
475
  */
369
- const trimmed = response.trim();
370
476
  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);
477
+ if (response.startsWith(FIVE_BACKTICKS) && response.endsWith(FIVE_BACKTICKS)) {
478
+ return response.slice(FIVE_BACKTICKS.length, -FIVE_BACKTICKS.length);
376
479
  }
377
- 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);
378
489
  }
379
490
 
380
491
  override async enhanceQuery(query: string, context: PerformanceTraceContext|null): Promise<string> {
@@ -426,15 +537,15 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
426
537
  }
427
538
 
428
539
  override async * run(initialQuery: string, options: {
429
- selected: ConversationContext<AgentFocus>|null,
540
+ selected: PerformanceTraceContext|null,
430
541
  signal?: AbortSignal,
431
542
  }): AsyncGenerator<ResponseData, void, void> {
432
543
  const focus = options.selected?.getItem();
433
544
 
434
545
  // Clear any previous facts in case the user changed the active context.
435
546
  this.clearFacts();
436
- if (focus) {
437
- this.#addFacts(focus);
547
+ if (options.selected && focus) {
548
+ this.#addFacts(options.selected);
438
549
  }
439
550
 
440
551
  return yield* super.run(initialQuery, options);
@@ -518,12 +629,18 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
518
629
  });
519
630
  }
520
631
 
521
- #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
+
522
639
  this.addFact(this.#callFrameDataDescriptionFact);
523
640
  this.addFact(this.#networkDataDescriptionFact);
524
641
 
525
642
  if (!this.#traceFacts.length) {
526
- this.#formatter = new PerformanceTraceFormatter(focus, this.#eventsSerializer);
643
+ this.#formatter = new PerformanceTraceFormatter(focus);
527
644
  this.#createFactForTraceSummary();
528
645
  this.#createFactForCriticalRequests();
529
646
  this.#createFactForMainThreadBottomUpSummary();
@@ -585,7 +702,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
585
702
  return {error: 'No insight available'};
586
703
  }
587
704
 
588
- const details = new PerformanceInsightFormatter(parsedTrace, insight).formatInsight();
705
+ const details = new PerformanceInsightFormatter(focus, insight).formatInsight();
589
706
 
590
707
  const key = `getInsightDetails('${params.insightName}')`;
591
708
  this.#cacheFunctionResult(focus, key, details);
@@ -613,7 +730,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
613
730
  },
614
731
  handler: async params => {
615
732
  debugLog('Function call: getEventByKey', params);
616
- const event = this.#lookupEvent(params.eventKey as Trace.Types.File.SerializableKey);
733
+ const event = focus.lookupEvent(params.eventKey as Trace.Types.File.SerializableKey);
617
734
  if (!event) {
618
735
  return {error: 'Invalid eventKey'};
619
736
  }
@@ -772,7 +889,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
772
889
  },
773
890
  },
774
891
  displayInfoFromArgs: args => {
775
- return {title: lockedString('Looking at call tree…'), action: `getDetailedCallTree(${args.eventKey})`};
892
+ return {title: lockedString('Looking at call tree…'), action: `getDetailedCallTree('${args.eventKey}')`};
776
893
  },
777
894
  handler: async args => {
778
895
  debugLog('Function call: getDetailedCallTree');
@@ -781,7 +898,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
781
898
  throw new Error('missing formatter');
782
899
  }
783
900
 
784
- const event = this.#lookupEvent(args.eventKey as Trace.Types.File.SerializableKey);
901
+ const event = focus.lookupEvent(args.eventKey as Trace.Types.File.SerializableKey);
785
902
  if (!event) {
786
903
  return {error: 'Invalid eventKey'};
787
904
  }
@@ -816,7 +933,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
816
933
  },
817
934
  },
818
935
  displayInfoFromArgs: args => {
819
- return {title: lockedString('Looking at resource content…'), action: `getResourceContent(${args.url})`};
936
+ return {title: lockedString('Looking at resource content…'), action: `getResourceContent('${args.url}')`};
820
937
  },
821
938
  handler: async args => {
822
939
  debugLog('Function call: getResourceContent');
@@ -829,14 +946,14 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
829
946
  }
830
947
  }
831
948
 
832
- const content = resource.content;
833
- if (!content) {
834
- 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}`};
835
952
  }
836
953
 
837
954
  const key = `getResourceContent(${args.url})`;
838
- this.#cacheFunctionResult(focus, key, content);
839
- return {result: {content}};
955
+ this.#cacheFunctionResult(focus, key, content.text);
956
+ return {result: {content: content.text}};
840
957
  },
841
958
 
842
959
  });
@@ -863,7 +980,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
863
980
  },
864
981
  handler: async params => {
865
982
  debugLog('Function call: selectEventByKey', params);
866
- const event = this.#lookupEvent(params.eventKey as Trace.Types.File.SerializableKey);
983
+ const event = focus.lookupEvent(params.eventKey as Trace.Types.File.SerializableKey);
867
984
  if (!event) {
868
985
  return {error: 'Invalid eventKey'};
869
986
  }