chrome-devtools-frontend 1.0.1632065 → 1.0.1635648

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 (49) hide show
  1. package/front_end/core/host/UserMetrics.ts +5 -2
  2. package/front_end/core/root/ExperimentNames.ts +1 -0
  3. package/front_end/core/root/Runtime.ts +5 -0
  4. package/front_end/entrypoints/main/MainImpl.ts +8 -0
  5. package/front_end/generated/InspectorBackendCommands.ts +3 -4
  6. package/front_end/generated/SupportedCSSProperties.js +272 -2
  7. package/front_end/generated/protocol-mapping.d.ts +0 -10
  8. package/front_end/generated/protocol-proxy-api.d.ts +0 -8
  9. package/front_end/generated/protocol.ts +5 -7
  10. package/front_end/models/ai_assistance/AiConversation.ts +16 -11
  11. package/front_end/models/ai_assistance/agents/AccessibilityAgent.ts +13 -2
  12. package/front_end/models/ai_assistance/agents/AiAgent.ts +65 -11
  13. package/front_end/models/ai_assistance/agents/ContextSelectionAgent.ts +42 -2
  14. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +30 -1
  15. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.ts +4 -2
  16. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.ts +5 -2
  17. package/front_end/models/extensions/RecorderExtensionEndpoint.ts +9 -1
  18. package/front_end/models/extensions/RecorderPluginManager.ts +1 -0
  19. package/front_end/models/trace/handlers/FramesHandler.ts +19 -13
  20. package/front_end/panels/ai_assistance/components/AccessibilityAgentMarkdownRenderer.ts +3 -0
  21. package/front_end/panels/ai_assistance/components/ChatInput.ts +4 -2
  22. package/front_end/panels/ai_assistance/components/ChatMessage.ts +3 -1
  23. package/front_end/panels/application/preloading/components/PreloadingString.ts +0 -8
  24. package/front_end/panels/common/ExtensionServer.ts +34 -11
  25. package/front_end/panels/elements/CSSRuleValidator.ts +37 -34
  26. package/front_end/panels/elements/CSSRuleValidatorHelper.ts +8 -6
  27. package/front_end/panels/elements/ElementsTreeElement.ts +8 -2
  28. package/front_end/panels/elements/components/CSSHintDetailsView.ts +5 -5
  29. package/front_end/panels/js_timeline/js_timeline-meta.ts +30 -0
  30. package/front_end/panels/protocol_monitor/JSONEditor.ts +4 -4
  31. package/front_end/panels/protocol_monitor/ProtocolMonitor.ts +2 -2
  32. package/front_end/panels/recorder/RecorderController.ts +50 -1
  33. package/front_end/panels/recorder/extensions/ExtensionManager.ts +1 -0
  34. package/front_end/panels/recorder/models/RecordingPlayer.ts +12 -3
  35. package/front_end/panels/recorder/testing/RecorderHelpers.ts +2 -0
  36. package/front_end/panels/sources/FilteredUISourceCodeListProvider.ts +5 -2
  37. package/front_end/panels/timeline/timeline-meta.ts +10 -6
  38. package/front_end/third_party/chromium/README.chromium +1 -1
  39. package/front_end/ui/legacy/InspectorDrawerView.ts +2 -1
  40. package/front_end/ui/legacy/InspectorView.ts +1 -1
  41. package/front_end/ui/legacy/PlusButton.ts +269 -0
  42. package/front_end/ui/legacy/ViewManager.ts +38 -11
  43. package/front_end/ui/legacy/components/source_frame/ImageView.ts +16 -0
  44. package/front_end/ui/legacy/components/source_frame/imageView.css +13 -0
  45. package/front_end/ui/legacy/components/utils/Linkifier.ts +1 -1
  46. package/front_end/ui/legacy/legacy.ts +2 -0
  47. package/front_end/ui/visual_logging/KnownContextValues.ts +19 -0
  48. package/mcp/mcp.ts +1 -1
  49. package/package.json +1 -1
@@ -429,16 +429,7 @@ export class AiConversation {
429
429
  throw new Error('cross-origin context data should not be included');
430
430
  }
431
431
 
432
- const userQuery: UserQuery = {
433
- type: ResponseType.USER_QUERY,
434
- query: initialQuery,
435
- imageInput: options.multimodalInput?.input,
436
- imageId: options.multimodalInput?.id,
437
- };
438
- void this.addHistoryItem(userQuery);
439
- yield userQuery;
440
-
441
- yield* this.#runAgent(initialQuery, options);
432
+ yield* this.#runAgent(initialQuery, options, {isInitialCall: true});
442
433
  } finally {
443
434
  targetManager.removeModelListener(
444
435
  SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.PrimaryPageChanged, listener, this);
@@ -456,6 +447,7 @@ export class AiConversation {
456
447
  signal?: AbortSignal,
457
448
  multimodalInput?: MultimodalInput,
458
449
  } = {},
450
+ runOptions: {isInitialCall?: boolean} = {},
459
451
  ): AsyncGenerator<ResponseData, void, void> {
460
452
  this.#setOriginIfEmpty(this.selectedContext?.getOrigin());
461
453
  if (this.isBlockedByOrigin) {
@@ -466,6 +458,17 @@ export class AiConversation {
466
458
  return;
467
459
  }
468
460
 
461
+ if (runOptions.isInitialCall) {
462
+ const userQuery: UserQuery = {
463
+ type: ResponseType.USER_QUERY,
464
+ query: initialQuery,
465
+ imageInput: options.multimodalInput?.input,
466
+ imageId: options.multimodalInput?.id,
467
+ };
468
+ void this.addHistoryItem(userQuery);
469
+ yield userQuery;
470
+ }
471
+
469
472
  function shouldAddToHistory(data: ResponseData): boolean {
470
473
  if (data.type === ResponseType.CONTEXT_CHANGE) {
471
474
  return false;
@@ -499,7 +502,9 @@ export class AiConversation {
499
502
  // requery with the specialized agent.
500
503
  if (data.type === ResponseType.CONTEXT_CHANGE) {
501
504
  this.setContext(data.context);
502
- yield* this.#runAgent(this.#getQueryAfterSelection(initialQuery, data.description), options);
505
+ yield*
506
+ this.#runAgent(
507
+ this.#getQueryAfterSelection(initialQuery, data.description), options, {isInitialCall: false});
503
508
  return;
504
509
  }
505
510
  }
@@ -236,7 +236,18 @@ export class AccessibilityAgent extends AiAgent<LHModel.ReporterTypes.ReportJSON
236
236
  if (!nodeId) {
237
237
  return null;
238
238
  }
239
- return domModel.nodeForId(nodeId);
239
+ const node = domModel.nodeForId(nodeId);
240
+ if (!node) {
241
+ return null;
242
+ }
243
+
244
+ const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
245
+ const mainFrameId = resourceTreeModel?.mainFrame?.id;
246
+ if (node.frameId() !== mainFrameId) {
247
+ return null;
248
+ }
249
+
250
+ return node;
240
251
  }
241
252
 
242
253
  #declareFunctions(): void {
@@ -281,7 +292,7 @@ export class AccessibilityAgent extends AiAgent<LHModel.ReporterTypes.ReportJSON
281
292
  const audits = new LighthouseFormatter().audits(report, 'accessibility');
282
293
  return {
283
294
  result: {audits},
284
- widgets: [{name: 'LIGHTHOUSE_REPORT', data: {report}}],
295
+ widgets: [{name: 'LIGHTHOUSE_REPORT', data: {report, snapshotReport: true}}],
285
296
  };
286
297
  }
287
298
  });
@@ -13,6 +13,8 @@ import type * as Trace from '../../trace/trace.js';
13
13
  import type * as Workspace from '../../workspace/workspace.js';
14
14
  import {debugLog, isStructuredLogEnabled} from '../debug.js';
15
15
 
16
+ const MAX_SUGGESTION_LENGTH = 200;
17
+
16
18
  export const enum ResponseType {
17
19
  CONTEXT = 'context',
18
20
  TITLE = 'title',
@@ -35,6 +37,21 @@ export const enum ErrorType {
35
37
  CROSS_ORIGIN = 'cross-origin'
36
38
  }
37
39
 
40
+ /**
41
+ * Returns true if the origin is considered opaque and should be blocked from
42
+ * AI assistance to prevent potential data leakage.
43
+ *
44
+ * @see https://crbug.com/513732588
45
+ */
46
+ export function isOpaqueOrigin(origin: string): boolean {
47
+ /**
48
+ * Origins starting with 'about' (like about:blank or about:srcdoc) are
49
+ * considered opaque. 'about://' is the sentinel used by DevTools
50
+ * ParsedURL.securityOrigin() for these.
51
+ */
52
+ return origin === 'null' || origin === 'data:' || origin.startsWith('about') || origin.startsWith('detached');
53
+ }
54
+
38
55
  export const enum MultimodalInputType {
39
56
  SCREENSHOT = 'screenshot',
40
57
  UPLOADED_IMAGE = 'uploaded-image',
@@ -185,15 +202,30 @@ export abstract class ConversationContext<T> {
185
202
  abstract getItem(): T;
186
203
  abstract getTitle(): string;
187
204
 
188
- isOriginAllowed(agentOrigin: string|undefined): boolean {
189
- if (!agentOrigin) {
205
+ /**
206
+ * Returns true if this data context (e.g., a DOM node or Network Request) is
207
+ * allowed to be included in a conversation that is locked to the provided
208
+ * `establishedOrigin`.
209
+ *
210
+ * A conversation is "locked" to an origin once the first query is made.
211
+ * This method ensures that we don't mix data from different origins in the
212
+ * same conversation.
213
+ *
214
+ * @param establishedOrigin The origin that the current conversation is locked to.
215
+ * If undefined, the conversation has not yet been locked to an origin.
216
+ */
217
+ isOriginAllowed(establishedOrigin: string|undefined): boolean {
218
+ const dataOrigin = this.getOrigin();
219
+ // Opaque origins are never allowed to be used as context.
220
+ if (isOpaqueOrigin(dataOrigin)) {
221
+ return false;
222
+ }
223
+ // If no origin is established yet, this context will be the one to lock the conversation.
224
+ if (!establishedOrigin) {
190
225
  return true;
191
226
  }
192
- // Currently does not handle opaque origins because they
193
- // are not available to DevTools, instead checks
194
- // that serialization of the origin is the same
195
- // https://html.spec.whatwg.org/#ascii-serialisation-of-an-origin.
196
- return this.getOrigin() === agentOrigin;
227
+ // Only allow data that matches the origin the conversation is already locked to.
228
+ return dataOrigin === establishedOrigin;
197
229
  }
198
230
 
199
231
  /**
@@ -294,6 +326,7 @@ export interface LighthouseReportAiWidget {
294
326
  name: 'LIGHTHOUSE_REPORT';
295
327
  data: {
296
328
  report: LHModel.ReporterTypes.ReportJSON,
329
+ snapshotReport?: boolean,
297
330
  };
298
331
  }
299
332
 
@@ -545,8 +578,7 @@ export abstract class AiAgent<T> {
545
578
  const trimmed = line.trim();
546
579
  if (trimmed.startsWith('SUGGESTIONS:')) {
547
580
  try {
548
- // TODO: Do basic validation this is an array with strings
549
- suggestions = JSON.parse(trimmed.substring('SUGGESTIONS:'.length).trim());
581
+ suggestions = sanitizeSuggestions(trimmed.substring('SUGGESTIONS:'.length).trim());
550
582
  } catch {
551
583
  }
552
584
  } else {
@@ -559,8 +591,7 @@ export abstract class AiAgent<T> {
559
591
  if (!suggestions && answerLines.at(-1)?.includes('SUGGESTIONS:')) {
560
592
  const [answer, suggestionsText] = answerLines[answerLines.length - 1].split('SUGGESTIONS:', 2);
561
593
  try {
562
- // TODO: Do basic validation this is an array with strings
563
- suggestions = JSON.parse(suggestionsText.trim().substring('SUGGESTIONS:'.length).trim());
594
+ suggestions = sanitizeSuggestions(suggestionsText.trim());
564
595
  } catch {
565
596
  }
566
597
  answerLines[answerLines.length - 1] = answer;
@@ -967,3 +998,26 @@ export abstract class AiAgent<T> {
967
998
  };
968
999
  }
969
1000
  }
1001
+
1002
+ function sanitizeSuggestions(suggestions: string): [string, ...string[]]|undefined {
1003
+ const parsed = JSON.parse(suggestions);
1004
+ if (!Array.isArray(parsed)) {
1005
+ return undefined;
1006
+ }
1007
+ const sanitized: string[] = [];
1008
+ for (const item of parsed) {
1009
+ if (typeof item !== 'string') {
1010
+ continue;
1011
+ }
1012
+ // Collapse multiple whitespace/newlines into a single space.
1013
+ const noExtraWhitespace = item.replace(/\s+/g, ' ').trim();
1014
+ if (noExtraWhitespace.length === 0) {
1015
+ continue;
1016
+ }
1017
+ sanitized.push(noExtraWhitespace.substring(0, MAX_SUGGESTION_LENGTH));
1018
+ }
1019
+ if (sanitized.length === 0) {
1020
+ return undefined;
1021
+ }
1022
+ return sanitized as [string, ...string[]];
1023
+ }
@@ -20,6 +20,7 @@ import {
20
20
  AiAgent,
21
21
  type AllowedOriginResult,
22
22
  type ContextResponse,
23
+ isOpaqueOrigin,
23
24
  type RequestOptions,
24
25
  } from './AiAgent.js';
25
26
  import {FileContext} from './FileAgent.js';
@@ -126,6 +127,11 @@ export class ContextSelectionAgent extends AiAgent<never> {
126
127
  };
127
128
  }
128
129
  const origin = allowedOriginResult.origin;
130
+ if (origin && isOpaqueOrigin(origin)) {
131
+ return {
132
+ error: 'No requests recorded by DevTools',
133
+ };
134
+ }
129
135
 
130
136
  let hasCrossOriginRequest = false;
131
137
  for (const request of Logs.NetworkLog.NetworkLog.instance().requests()) {
@@ -196,6 +202,11 @@ export class ContextSelectionAgent extends AiAgent<never> {
196
202
  };
197
203
  }
198
204
  const origin = allowedOriginResult.origin;
205
+ if (origin && isOpaqueOrigin(origin)) {
206
+ return {
207
+ error: 'No request found',
208
+ };
209
+ }
199
210
  const request = Logs.NetworkLog.NetworkLog.instance().requests().find(req => {
200
211
  if (req.requestId() !== id) {
201
212
  return false;
@@ -235,8 +246,23 @@ export class ContextSelectionAgent extends AiAgent<never> {
235
246
  };
236
247
  },
237
248
  handler: async () => {
249
+ const allowedOriginResult = this.#allowedOrigin();
250
+ if ('blocked' in allowedOriginResult) {
251
+ return {
252
+ error: 'Cross-origin access blocked due to navigation. Please start a new chat.',
253
+ };
254
+ }
255
+ const origin = allowedOriginResult.origin;
256
+
238
257
  const files: Array<{file: string, id: number | undefined}> = [];
239
258
  for (const file of ContextSelectionAgent.getUISourceCodes()) {
259
+ const fileUrl = file.url();
260
+ const fileOrigin = Common.ParsedURL.ParsedURL.extractOrigin(fileUrl);
261
+
262
+ if (origin && fileOrigin !== origin) {
263
+ continue;
264
+ }
265
+
240
266
  files.push({
241
267
  file: file.fullDisplayName(),
242
268
  id: ContextSelectionAgent.uiSourceCodeId.get(file),
@@ -272,8 +298,22 @@ export class ContextSelectionAgent extends AiAgent<never> {
272
298
  };
273
299
  },
274
300
  handler: async params => {
275
- const file = ContextSelectionAgent.getUISourceCodes().find(
276
- file => ContextSelectionAgent.uiSourceCodeId.get(file) === params.id);
301
+ const allowedOriginResult = this.#allowedOrigin();
302
+ if ('blocked' in allowedOriginResult) {
303
+ return {
304
+ error: 'Cross-origin access blocked due to navigation. Please start a new chat.',
305
+ };
306
+ }
307
+ const origin = allowedOriginResult.origin;
308
+
309
+ const file = ContextSelectionAgent.getUISourceCodes().find(file => {
310
+ if (ContextSelectionAgent.uiSourceCodeId.get(file) !== params.id) {
311
+ return false;
312
+ }
313
+ const fileUrl = file.url();
314
+ const fileOrigin = Common.ParsedURL.ParsedURL.extractOrigin(fileUrl);
315
+ return !origin || fileOrigin === origin;
316
+ });
277
317
 
278
318
  if (!file) {
279
319
  return {
@@ -14,6 +14,7 @@ import * as Logs from '../../logs/logs.js';
14
14
  import * as SourceMapScopes from '../../source_map_scopes/source_map_scopes.js';
15
15
  import * as TextUtils from '../../text_utils/text_utils.js';
16
16
  import * as Trace from '../../trace/trace.js';
17
+ import {sanitizeHeaders} from '../data_formatters/NetworkRequestFormatter.js';
17
18
  import {
18
19
  PerformanceInsightFormatter,
19
20
  } from '../data_formatters/PerformanceInsightFormatter.js';
@@ -1060,7 +1061,35 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
1060
1061
  }
1061
1062
 
1062
1063
  // TODO(b/425270067): Format in the same way that "Summary" detail tab does.
1063
- const details = JSON.stringify(event);
1064
+ let details;
1065
+ if (Trace.Types.Events.isSyntheticNetworkRequest(event)) {
1066
+ const eventToSerialize = {
1067
+ ...event,
1068
+ args: {
1069
+ ...event.args,
1070
+ data: {
1071
+ ...event.args.data,
1072
+ responseHeaders: event.args.data.responseHeaders ? sanitizeHeaders(event.args.data.responseHeaders) :
1073
+ null,
1074
+ },
1075
+ },
1076
+ };
1077
+ details = JSON.stringify(eventToSerialize);
1078
+ } else if (Trace.Types.Events.isResourceReceiveResponse(event)) {
1079
+ const eventToSerialize = {
1080
+ ...event,
1081
+ args: {
1082
+ ...event.args,
1083
+ data: {
1084
+ ...event.args.data,
1085
+ headers: event.args.data.headers ? sanitizeHeaders(event.args.data.headers) : undefined,
1086
+ },
1087
+ },
1088
+ };
1089
+ details = JSON.stringify(eventToSerialize);
1090
+ } else {
1091
+ details = JSON.stringify(event);
1092
+ }
1064
1093
 
1065
1094
  const key = `getEventByKey('${params.eventKey}')`;
1066
1095
  this.#cacheFunctionResult(focus, key, details);
@@ -3,6 +3,7 @@
3
3
  // found in the LICENSE file.
4
4
 
5
5
  import * as Common from '../../../core/common/common.js';
6
+ import type * as CrUXManager from '../../crux-manager/crux-manager.js';
6
7
  import * as Trace from '../../trace/trace.js';
7
8
  import type {ConversationSuggestions} from '../agents/AiAgent.js';
8
9
  import type {AgentFocus} from '../performance/AIContext.js';
@@ -48,8 +49,9 @@ export class PerformanceInsightFormatter {
48
49
  #insight: Trace.Insights.Types.InsightModel;
49
50
  #parsedTrace: Trace.TraceModel.ParsedTrace;
50
51
 
51
- constructor(focus: AgentFocus, insight: Trace.Insights.Types.InsightModel) {
52
- this.#traceFormatter = new PerformanceTraceFormatter(focus);
52
+ constructor(
53
+ focus: AgentFocus, insight: Trace.Insights.Types.InsightModel, deviceScope: CrUXManager.DeviceScope|null = null) {
54
+ this.#traceFormatter = new PerformanceTraceFormatter(focus, deviceScope);
53
55
  this.#insight = insight;
54
56
  this.#parsedTrace = focus.parsedTrace;
55
57
  }
@@ -35,15 +35,17 @@ export class PerformanceTraceFormatter {
35
35
  #insightSet: Trace.Insights.Types.InsightSet|null;
36
36
  #eventsSerializer: Trace.EventsSerializer.EventsSerializer;
37
37
  #formattedFunctionCodes = new Set<string>();
38
+ #deviceScope: CrUXManager.DeviceScope|null;
38
39
  resolveFunctionCode?:
39
40
  (url: Platform.DevToolsPath.UrlString, line: number,
40
41
  column: number) => Promise<SourceMapScopes.FunctionCodeResolver.FunctionCode|null>;
41
42
 
42
- constructor(focus: AgentFocus) {
43
+ constructor(focus: AgentFocus, deviceScope: CrUXManager.DeviceScope|null = null) {
43
44
  this.#focus = focus;
44
45
  this.#parsedTrace = focus.parsedTrace;
45
46
  this.#insightSet = focus.primaryInsightSet;
46
47
  this.#eventsSerializer = focus.eventsSerializer;
48
+ this.#deviceScope = deviceScope;
47
49
  }
48
50
 
49
51
  serializeEvent(event: Trace.Types.Events.Event): string {
@@ -64,7 +66,8 @@ export class PerformanceTraceFormatter {
64
66
  return [];
65
67
  }
66
68
  try {
67
- const cruxScope = CrUXManager.CrUXManager.instance().getSelectedScope();
69
+ const cruxScope: CrUXManager.Scope = this.#deviceScope ? {pageScope: 'url', deviceScope: this.#deviceScope} :
70
+ CrUXManager.CrUXManager.instance().getSelectedScope();
68
71
  const parts: string[] = [];
69
72
  const fieldMetrics =
70
73
  Trace.Insights.Common.getFieldMetricsForInsightSet(insightSet, this.#parsedTrace.metadata, cruxScope);
@@ -2,6 +2,8 @@
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 type * as Platform from '../../core/platform/platform.js';
6
+
5
7
  import {PrivateAPI} from './ExtensionAPI.js';
6
8
  import {ExtensionEndpoint} from './ExtensionEndpoint.js';
7
9
  import {RecorderPluginManager} from './RecorderPluginManager.js';
@@ -10,20 +12,26 @@ export class RecorderExtensionEndpoint extends ExtensionEndpoint {
10
12
  private readonly name: string;
11
13
  private readonly mediaType?: string;
12
14
  private readonly capabilities: PrivateAPI.RecordingExtensionPluginCapability[];
15
+ readonly #extensionOrigin: Platform.DevToolsPath.UrlString;
13
16
 
14
17
  constructor(
15
18
  name: string, port: MessagePort, capabilities: PrivateAPI.RecordingExtensionPluginCapability[],
16
- mediaType?: string) {
19
+ extensionOrigin: Platform.DevToolsPath.UrlString, mediaType?: string) {
17
20
  super(port);
18
21
  this.name = name;
19
22
  this.mediaType = mediaType;
20
23
  this.capabilities = capabilities;
24
+ this.#extensionOrigin = extensionOrigin;
21
25
  }
22
26
 
23
27
  getName(): string {
24
28
  return this.name;
25
29
  }
26
30
 
31
+ getOrigin(): Platform.DevToolsPath.UrlString {
32
+ return this.#extensionOrigin;
33
+ }
34
+
27
35
  getCapabilities(): PrivateAPI.RecordingExtensionPluginCapability[] {
28
36
  return this.capabilities;
29
37
  }
@@ -14,6 +14,7 @@ export interface ViewDescriptor {
14
14
  pagePath: string;
15
15
  onShown: () => void;
16
16
  onHidden: () => void;
17
+ extensionOrigin: string;
17
18
  }
18
19
 
19
20
  export class RecorderPluginManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
@@ -507,26 +507,28 @@ export class TimelineFrameBeginFrameQueue {
507
507
  private queueFrames: number[] = [];
508
508
 
509
509
  // Maps frameSeqId to BeginFrameInfo.
510
- private mapFrames: Record<number, BeginFrameInfo> = {};
510
+ private mapFrames = new Map<number, BeginFrameInfo>();
511
511
 
512
512
  // Add a BeginFrame to the queue, if it does not already exit.
513
513
  addFrameIfNotExists(seqId: number, startTime: Types.Timing.Micro, isDropped: boolean, isPartial: boolean): void {
514
- if (!(seqId in this.mapFrames)) {
515
- this.mapFrames[seqId] = new BeginFrameInfo(seqId, startTime, isDropped, isPartial);
514
+ if (!this.mapFrames.has(seqId)) {
515
+ this.mapFrames.set(seqId, new BeginFrameInfo(seqId, startTime, isDropped, isPartial));
516
516
  this.queueFrames.push(seqId);
517
517
  }
518
518
  }
519
519
 
520
520
  // Set a BeginFrame in queue as dropped.
521
521
  setDropped(seqId: number, isDropped: boolean): void {
522
- if (seqId in this.mapFrames) {
523
- this.mapFrames[seqId].isDropped = isDropped;
522
+ const frame = this.mapFrames.get(seqId);
523
+ if (frame) {
524
+ frame.isDropped = isDropped;
524
525
  }
525
526
  }
526
527
 
527
528
  setPartial(seqId: number, isPartial: boolean): void {
528
- if (seqId in this.mapFrames) {
529
- this.mapFrames[seqId].isPartial = isPartial;
529
+ const frame = this.mapFrames.get(seqId);
530
+ if (frame) {
531
+ frame.isPartial = isPartial;
530
532
  }
531
533
  }
532
534
 
@@ -535,7 +537,7 @@ export class TimelineFrameBeginFrameQueue {
535
537
 
536
538
  // Do not visualize this frame in the rare case where the current DrawFrame
537
539
  // does not have a corresponding BeginFrame.
538
- if (seqId in this.mapFrames) {
540
+ if (this.mapFrames.has(seqId)) {
539
541
  // Pop all BeginFrames before the current frame, and add only the dropped
540
542
  // ones in |frames_to_visualize|.
541
543
  // Non-dropped frames popped here are BeginFrames that are never
@@ -544,17 +546,21 @@ export class TimelineFrameBeginFrameQueue {
544
546
  // be naturally presented as continuationss of other frames.
545
547
  while (this.queueFrames[0] !== seqId) {
546
548
  const currentSeqId = this.queueFrames[0];
547
- if (this.mapFrames[currentSeqId].isDropped) {
548
- framesToVisualize.push(this.mapFrames[currentSeqId]);
549
+ const currentFrame = this.mapFrames.get(currentSeqId);
550
+ if (currentFrame && currentFrame.isDropped) {
551
+ framesToVisualize.push(currentFrame);
549
552
  }
550
553
 
551
- delete this.mapFrames[currentSeqId];
554
+ this.mapFrames.delete(currentSeqId);
552
555
  this.queueFrames.shift();
553
556
  }
554
557
 
555
558
  // Pop the BeginFrame associated with the current DrawFrame.
556
- framesToVisualize.push(this.mapFrames[seqId]);
557
- delete this.mapFrames[seqId];
559
+ const frame = this.mapFrames.get(seqId);
560
+ if (frame) {
561
+ framesToVisualize.push(frame);
562
+ }
563
+ this.mapFrames.delete(seqId);
558
564
  this.queueFrames.shift();
559
565
  }
560
566
  return framesToVisualize;
@@ -123,6 +123,9 @@ export class AccessibilityAgentMarkdownRenderer extends MarkdownRendererWithCode
123
123
  if (!node) {
124
124
  return;
125
125
  }
126
+ if (node.frameId() !== this.mainFrameId) {
127
+ return;
128
+ }
126
129
  const linkedNode = PanelsCommon.DOMLinkifier.Linkifier.instance().linkify(node, {textContent: label});
127
130
  return linkedNode;
128
131
  }
@@ -530,9 +530,11 @@ export class ChatInput extends UI.Widget.Widget implements SDK.TargetManager.Obs
530
530
 
531
531
  setInputValue(text: string): void {
532
532
  if (this.#textAreaRef.value) {
533
- this.#textAreaRef.value.value = text;
533
+ const maxLength = this.#textAreaRef.value.maxLength;
534
+ const truncatedText = (maxLength >= 0) ? text.substring(0, maxLength) : text;
535
+ this.#textAreaRef.value.value = truncatedText;
534
536
  // Place the cursor at the end of the new value.
535
- this.#textAreaRef.value.setSelectionRange(text.length, text.length);
537
+ this.#textAreaRef.value.setSelectionRange(truncatedText.length, truncatedText.length);
536
538
  }
537
539
  this.performUpdate();
538
540
  }
@@ -2090,11 +2090,13 @@ async function makeLighthouseReportWidget(widgetData: LighthouseReportAiWidget):
2090
2090
  return null;
2091
2091
  }
2092
2092
 
2093
+ const snapshotReport = widgetData.data.snapshotReport;
2094
+
2093
2095
  return {
2094
2096
  renderedWidget: html`<div class="lighthouse-report-widget">${reportEl}</div>`,
2095
2097
  revealable: new Lighthouse.LighthousePanel.ActiveLighthouseReport(widgetData.data.report),
2096
2098
  accessibleRevealLabel: lockedString(UIStringsNotTranslate.revealLighthouse),
2097
2099
  title: lockedString(UIStringsNotTranslate.lighthouseReport),
2098
- jslogContext: 'lighthouse-report-widget',
2100
+ jslogContext: snapshotReport ? 'lighthouse-snapshot-report-widget' : 'lighthouse-report-widget',
2099
2101
  };
2100
2102
  }
@@ -11,11 +11,6 @@ import * as Protocol from '../../../../generated/protocol.js';
11
11
  import * as Bindings from '../../../../models/bindings/bindings.js';
12
12
 
13
13
  const UIStrings = {
14
- /**
15
- * @description Descrption text for Prefetch status PrefetchCancelledOnUserNavigation.
16
- */
17
- PrefetchCancelledOnUserNavigation:
18
- 'The prefetch was cancelled because the user navigated the page before the prefetch finished',
19
14
  /**
20
15
  * @description Description text for Prefetch status PrefetchFailedIneligibleRedirect.
21
16
  */
@@ -459,7 +454,6 @@ export const PrefetchReasonDescription: Record<string, {name: () => Platform.UIS
459
454
  PrefetchNotEligibleRedirectFromServiceWorker: {name: () => i18n.i18n.lockedString('Unknown')},
460
455
  PrefetchNotEligibleRedirectToServiceWorker: {name: () => i18n.i18n.lockedString('Unknown')},
461
456
  PrefetchEvictedAfterBrowsingDataRemoved: {name: i18nLazyString(UIStrings.PrefetchEvictedAfterBrowsingDataRemoved)},
462
- PrefetchCancelledOnUserNavigation: {name: i18nLazyString(UIStrings.PrefetchCancelledOnUserNavigation)},
463
457
  };
464
458
 
465
459
  /** Decoding PrefetchFinalStatus prefetchAttempt to failure description. **/
@@ -546,8 +540,6 @@ export function prefetchFailureReason(
546
540
  return PrefetchReasonDescription['PrefetchNotEligibleRedirectToServiceWorker'].name();
547
541
  case Protocol.Preload.PrefetchStatus.PrefetchEvictedAfterBrowsingDataRemoved:
548
542
  return PrefetchReasonDescription['PrefetchEvictedAfterBrowsingDataRemoved'].name();
549
- case Protocol.Preload.PrefetchStatus.PrefetchCancelledOnUserNavigation:
550
- return PrefetchReasonDescription['PrefetchCancelledOnUserNavigation'].name();
551
543
  default:
552
544
  // Note that we use switch and exhaustiveness check to prevent to
553
545
  // forget updating these strings, but allow to handle unknown