chrome-devtools-frontend 1.0.1574367 → 1.0.1575174

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.
package/AUTHORS CHANGED
@@ -64,6 +64,7 @@ Krishnal Ciccolella <ciccolella.krishnal@gmail.com>
64
64
  Liam DeBeasi <ldebeasi@gmail.com>
65
65
  Luke Swiderski <luke.swiderski@gmail.com>
66
66
  Luke Warlow <luke@warlow.dev>
67
+ Lyra Rebane <rebane2001@gmail.com>
67
68
  Marijn Haverbeke <marijnh@gmail.com>
68
69
  Max 😎 Coplan <mchcopl@gmail.com>
69
70
  Michael Brüning <michael.bruning@qt.io>
@@ -12,6 +12,9 @@ Adding new DevTools experiments is deprecated, the preferred way for adding new
12
12
  features / exposing experimental features is via `base::Feature`s. These are
13
13
  controllable via Chromium command line parameters or optionally via `chrome://flags`.
14
14
 
15
+ Note: We are currently in the process of migrating away from DevTools experiments,
16
+ this documentation is partly outdated and will be updated ASAP.
17
+
15
18
 
16
19
  [TOC]
17
20
 
@@ -76,9 +79,9 @@ This step is optional. If you want the `base::Feature` to be controllable via th
76
79
 
77
80
  Please refer to this [example CL](https://crrev.com/c/5626314).
78
81
 
79
- ## How to add experiments
82
+ ## DEPRECATED:How to add experiments
80
83
 
81
- Note: Adding new DevTools experiments is deprecated, please use a `base::Feature` instead.
84
+ Note: We are currently in the process of migrating away from DevTools experiments, please use a `base::Feature` instead.
82
85
 
83
86
  If you want to launch a new feature in DevTools behind an experiment flag, you
84
87
  will need to do two things:
@@ -343,7 +343,7 @@ export class SettingsStorage {
343
343
  export class Deprecation {
344
344
  readonly disabled: boolean;
345
345
  readonly warning: Platform.UIString.LocalizedString;
346
- readonly experiment?: Root.Runtime.Experiment;
346
+ readonly experiment?: Root.Runtime.Experiment|Root.Runtime.HostExperiment;
347
347
 
348
348
  constructor({deprecationNotice}: SettingRegistration) {
349
349
  if (!deprecationNotice) {
@@ -389,6 +389,8 @@ export interface InspectorFrontendHostAPI {
389
389
 
390
390
  recordPerformanceHistogram(histogramName: string, duration: number): void;
391
391
 
392
+ recordPerformanceHistogramMedium(histogramName: string, duration: number): void;
393
+
392
394
  recordUserMetricsAction(umaName: string): void;
393
395
 
394
396
  recordNewBadgeUsage(featureName: string): void;
@@ -449,6 +451,8 @@ export interface InspectorFrontendHostAPI {
449
451
  recordKeyDown(event: KeyDownEvent): void;
450
452
  recordSettingAccess(event: SettingAccessEvent): void;
451
453
  recordFunctionCall(event: FunctionCallEvent): void;
454
+
455
+ setChromeFlag(flagName: string, value: boolean): void;
452
456
  }
453
457
 
454
458
  export interface AcceleratorDescriptor {
@@ -512,7 +516,7 @@ export interface SyncInformation {
512
516
  }
513
517
 
514
518
  /**
515
- * Enum for recordPerformanceHistogram
519
+ * Enum for recordEnumeratedHistogram
516
520
  * Warning: There is another definition of this enum in the DevTools code
517
521
  * base, keep them in sync:
518
522
  * front_end/devtools_compatibility.js
@@ -255,6 +255,13 @@ export class InspectorFrontendHostStub implements InspectorFrontendHostAPI {
255
255
  this.recordedPerformanceHistograms.push({histogramName, duration});
256
256
  }
257
257
 
258
+ recordPerformanceHistogramMedium(histogramName: string, duration: number): void {
259
+ if (this.recordedPerformanceHistograms.length >= MAX_RECORDED_HISTOGRAMS_SIZE) {
260
+ this.recordedPerformanceHistograms.shift();
261
+ }
262
+ this.recordedPerformanceHistograms.push({histogramName, duration});
263
+ }
264
+
258
265
  recordUserMetricsAction(_umaName: string): void {
259
266
  }
260
267
 
@@ -555,4 +562,7 @@ export class InspectorFrontendHostStub implements InspectorFrontendHostAPI {
555
562
  }
556
563
  recordFunctionCall(_event: FunctionCallEvent): void {
557
564
  }
565
+
566
+ setChromeFlag(_flagName: string, _value: boolean): void {
567
+ }
558
568
  }
@@ -283,11 +283,26 @@ export class UserMetrics {
283
283
  'DevTools.Insights.TeaserGenerationTime', timeInMilliseconds);
284
284
  }
285
285
 
286
+ consoleInsightTeaserGeneratedMedium(timeInMilliseconds: number): void {
287
+ InspectorFrontendHostInstance.recordPerformanceHistogramMedium(
288
+ 'DevTools.Insights.TeaserGenerationTimeMedium', timeInMilliseconds);
289
+ }
290
+
286
291
  consoleInsightTeaserFirstChunkGenerated(timeInMilliseconds: number): void {
287
292
  InspectorFrontendHostInstance.recordPerformanceHistogram(
288
293
  'DevTools.Insights.TeaserFirstChunkGenerationTime', timeInMilliseconds);
289
294
  }
290
295
 
296
+ consoleInsightTeaserFirstChunkGeneratedMedium(timeInMilliseconds: number): void {
297
+ InspectorFrontendHostInstance.recordPerformanceHistogramMedium(
298
+ 'DevTools.Insights.TeaserFirstChunkGenerationTimeMedium', timeInMilliseconds);
299
+ }
300
+
301
+ consoleInsightTeaserChunkToEndMedium(timeInMilliseconds: number): void {
302
+ InspectorFrontendHostInstance.recordPerformanceHistogramMedium(
303
+ 'DevTools.Insights.TeaserChunkToEndMedium', timeInMilliseconds);
304
+ }
305
+
291
306
  consoleInsightTeaserAbortedAfterFirstCharacter(timeInMilliseconds: number): void {
292
307
  InspectorFrontendHostInstance.recordPerformanceHistogram(
293
308
  'DevTools.Insights.TeaserAfterFirstCharacterAbortionTime', timeInMilliseconds);
@@ -159,24 +159,35 @@ export interface Option {
159
159
 
160
160
  export class ExperimentsSupport {
161
161
  #experiments: Experiment[] = [];
162
+ #hostExperiments = new Map<ExperimentName, HostExperiment>();
162
163
  readonly #experimentNames = new Set<ExperimentName>();
163
- readonly #enabledTransiently = new Set<ExperimentName>();
164
+ readonly #enabledForTests = new Set<ExperimentName>();
164
165
  readonly #enabledByDefault = new Set<ExperimentName>();
165
166
  readonly #serverEnabled = new Set<ExperimentName>();
166
167
  readonly #storage = new ExperimentStorage();
167
168
 
168
- allConfigurableExperiments(): Experiment[] {
169
- const result = [];
170
- for (const experiment of this.#experiments) {
171
- if (!this.#enabledTransiently.has(experiment.name)) {
172
- result.push(experiment);
173
- }
169
+ allConfigurableExperiments(): Array<Experiment|HostExperiment> {
170
+ return [...this.#experiments, ...this.#hostExperiments.values()];
171
+ }
172
+
173
+ registerHostExperiment(params: {
174
+ name: ExperimentName,
175
+ title: string,
176
+ aboutFlag: string,
177
+ isEnabled: boolean,
178
+ docLink?: Platform.DevToolsPath.UrlString,
179
+ readonly feedbackLink?: Platform.DevToolsPath.UrlString,
180
+ }): HostExperiment {
181
+ if (this.#isHostExperiment(params.name) || this.#isExperiment(params.name)) {
182
+ throw new Error(`Duplicate registration of experiment '${params.name}'`);
174
183
  }
175
- return result;
184
+ const hostExperiment = new HostExperiment({...params, experiments: this});
185
+ this.#hostExperiments.set(params.name, hostExperiment);
186
+ return hostExperiment;
176
187
  }
177
188
 
178
189
  register(experimentName: ExperimentName, experimentTitle: string, docLink?: string, feedbackLink?: string): void {
179
- if (this.#experimentNames.has(experimentName)) {
190
+ if (this.#isHostExperiment(experimentName) || this.#isExperiment(experimentName)) {
180
191
  throw new Error(`Duplicate registration of experiment '${experimentName}'`);
181
192
  }
182
193
  this.#experimentNames.add(experimentName);
@@ -187,63 +198,87 @@ export class ExperimentsSupport {
187
198
  }
188
199
 
189
200
  isEnabled(experimentName: ExperimentName): boolean {
190
- this.checkExperiment(experimentName);
191
- // Check for explicitly disabled #experiments first - the code could call setEnable(false) on the experiment enabled
192
- // by default and we should respect that.
193
- if (this.#storage.get(experimentName) === false) {
194
- return false;
201
+ if (this.#isHostExperiment(experimentName)) {
202
+ return this.#enabledForTests.has(experimentName) ||
203
+ (this.#hostExperiments.get(experimentName)?.isEnabled() ?? false);
195
204
  }
196
- if (this.#enabledTransiently.has(experimentName) || this.#enabledByDefault.has(experimentName)) {
197
- return true;
198
- }
199
- if (this.#serverEnabled.has(experimentName)) {
200
- return true;
205
+ if (this.#isExperiment(experimentName)) {
206
+ // Check for explicitly disabled #experiments first - the code could call setEnable(false)
207
+ // on the experiment enabled by default and we should respect that.
208
+ if (this.#storage.get(experimentName) === false) {
209
+ return false;
210
+ }
211
+ if (this.#enabledForTests.has(experimentName) || this.#enabledByDefault.has(experimentName)) {
212
+ return true;
213
+ }
214
+ if (this.#serverEnabled.has(experimentName)) {
215
+ return true;
216
+ }
217
+ return Boolean(this.#storage.get(experimentName));
201
218
  }
202
-
203
- return Boolean(this.#storage.get(experimentName));
219
+ throw new Error(`Unknown experiment '${experimentName}'`);
204
220
  }
205
221
 
206
- setEnabled(experimentName: ExperimentName, enabled: boolean): void {
207
- this.checkExperiment(experimentName);
208
- this.#storage.set(experimentName, enabled);
222
+ getValueFromStorage(experimentName: ExperimentName): boolean|undefined {
223
+ return this.#storage.get(experimentName);
209
224
  }
210
225
 
211
- enableExperimentsTransiently(experimentNames: ExperimentName[]): void {
212
- for (const experimentName of experimentNames) {
213
- this.checkExperiment(experimentName);
214
- this.#enabledTransiently.add(experimentName);
226
+ setEnabled(experimentName: ExperimentName, enabled: boolean): void {
227
+ if (this.#isHostExperiment(experimentName)) {
228
+ this.#hostExperiments.get(experimentName)?.setEnabled(enabled);
229
+ return;
230
+ }
231
+ if (this.#isExperiment(experimentName)) {
232
+ this.#storage.set(experimentName, enabled);
233
+ return;
215
234
  }
235
+ throw new Error(`Unknown experiment '${experimentName}'`);
216
236
  }
217
237
 
238
+ // Only applicable to legacy experiments.
218
239
  enableExperimentsByDefault(experimentNames: ExperimentName[]): void {
219
240
  for (const experimentName of experimentNames) {
220
- this.checkExperiment(experimentName);
241
+ if (!this.#isExperiment(experimentName)) {
242
+ throw new Error(`Unknown (legacy) experiment '${experimentName}'`);
243
+ }
221
244
  this.#enabledByDefault.add(experimentName);
222
245
  }
223
246
  }
224
247
 
248
+ // Only applicable to legacy experiments.
225
249
  setServerEnabledExperiments(experiments: string[]): void {
226
250
  for (const experiment of experiments) {
227
251
  const experimentName = experiment as ExperimentName;
228
- this.checkExperiment(experimentName);
252
+ if (!this.#isExperiment(experimentName)) {
253
+ throw new Error(`Unknown (legacy) experiment '${experimentName}'`);
254
+ }
229
255
  this.#serverEnabled.add(experimentName);
230
256
  }
231
257
  }
232
258
 
233
259
  enableForTest(experimentName: ExperimentName): void {
234
- this.checkExperiment(experimentName);
235
- this.#enabledTransiently.add(experimentName);
260
+ if (!this.#isHostExperiment(experimentName) && !this.#isExperiment(experimentName)) {
261
+ throw new Error(`Unknown experiment '${experimentName}'`);
262
+ }
263
+ this.#enabledForTests.add(experimentName);
236
264
  }
237
265
 
238
266
  disableForTest(experimentName: ExperimentName): void {
239
- this.checkExperiment(experimentName);
240
- this.#enabledTransiently.delete(experimentName);
267
+ if (!this.#isHostExperiment(experimentName) && !this.#isExperiment(experimentName)) {
268
+ throw new Error(`Unknown experiment '${experimentName}'`);
269
+ }
270
+ this.#enabledForTests.delete(experimentName);
271
+ }
272
+
273
+ isEnabledForTest(experimentName: ExperimentName): boolean {
274
+ return this.#enabledForTests.has(experimentName);
241
275
  }
242
276
 
243
277
  clearForTest(): void {
244
278
  this.#experiments = [];
279
+ this.#hostExperiments.clear();
245
280
  this.#experimentNames.clear();
246
- this.#enabledTransiently.clear();
281
+ this.#enabledForTests.clear();
247
282
  this.#enabledByDefault.clear();
248
283
  this.#serverEnabled.clear();
249
284
  }
@@ -252,10 +287,12 @@ export class ExperimentsSupport {
252
287
  this.#storage.cleanUpStaleExperiments(this.#experimentNames);
253
288
  }
254
289
 
255
- private checkExperiment(experimentName: ExperimentName): void {
256
- if (!this.#experimentNames.has(experimentName)) {
257
- throw new Error(`Unknown experiment '${experimentName}'`);
258
- }
290
+ #isHostExperiment(experimentName: ExperimentName): boolean {
291
+ return this.#hostExperiments.has(experimentName);
292
+ }
293
+
294
+ #isExperiment(experimentName: ExperimentName): boolean {
295
+ return this.#experimentNames.has(experimentName);
259
296
  }
260
297
  }
261
298
 
@@ -332,6 +369,44 @@ export class Experiment {
332
369
  }
333
370
  }
334
371
 
372
+ export class HostExperiment {
373
+ name: ExperimentName;
374
+ title: string;
375
+ readonly #experiments: ExperimentsSupport;
376
+ // This is the name of the corresponding Chromium flag (in chrome/browser/about_flags.cc).
377
+ // It is NOT the the name of the corresponding Chromium `base::Feature`.
378
+ aboutFlag: string;
379
+ #isEnabled: boolean;
380
+ docLink?: Platform.DevToolsPath.UrlString;
381
+ readonly feedbackLink?: Platform.DevToolsPath.UrlString;
382
+
383
+ constructor(params: {
384
+ name: ExperimentName,
385
+ title: string,
386
+ experiments: ExperimentsSupport,
387
+ aboutFlag: string,
388
+ isEnabled: boolean,
389
+ docLink?: Platform.DevToolsPath.UrlString,
390
+ feedbackLink?: Platform.DevToolsPath.UrlString,
391
+ }) {
392
+ this.name = params.name;
393
+ this.title = params.title;
394
+ this.#experiments = params.experiments;
395
+ this.aboutFlag = params.aboutFlag;
396
+ this.#isEnabled = params.isEnabled;
397
+ this.docLink = params.docLink;
398
+ this.feedbackLink = params.feedbackLink;
399
+ }
400
+
401
+ isEnabled(): boolean {
402
+ return this.#experiments.isEnabledForTest(this.name) || this.#isEnabled;
403
+ }
404
+
405
+ setEnabled(enabled: boolean): void {
406
+ this.#isEnabled = enabled;
407
+ }
408
+ }
409
+
335
410
  /** This must be constructed after the query parameters have been parsed. **/
336
411
  export const experiments = new ExperimentsSupport();
337
412
 
@@ -524,6 +599,10 @@ interface ConsoleInsightsTeasers {
524
599
  allowWithoutGpu: boolean;
525
600
  }
526
601
 
602
+ interface DevToolsProtocolMonitor {
603
+ enabled: boolean;
604
+ }
605
+
527
606
  /**
528
607
  * The host configuration that we expect from the DevTools back-end.
529
608
  *
@@ -575,6 +654,7 @@ export type HostConfig = Platform.TypeScriptUtilities.RecursivePartial<{
575
654
  devToolsAiAssistanceContextSelectionAgent: HostConfigAiAssistanceContextSelectionAgent,
576
655
  devToolsConsoleInsightsTeasers: ConsoleInsightsTeasers,
577
656
  devToolsGeminiRebranding: HostConfigGeminiRebranding,
657
+ devToolsProtocolMonitor: DevToolsProtocolMonitor,
578
658
  }>;
579
659
 
580
660
  /**
@@ -328,6 +328,19 @@ export class MainImpl {
328
328
  return {syncedStorage, globalStorage, localStorage};
329
329
  }
330
330
 
331
+ // eslint-disable-next-line no-unused-private-class-members
332
+ #migrateValueFromLegacyToHostExperiment(
333
+ legacyExperimentName: Root.ExperimentNames.ExperimentName, hostExperiment: Root.Runtime.HostExperiment): void {
334
+ const value = Root.Runtime.experiments.getValueFromStorage(legacyExperimentName);
335
+ if (value !== undefined && hostExperiment.aboutFlag) {
336
+ // Set the host experiment to the same value as the legacy experiment.
337
+ hostExperiment.setEnabled(value);
338
+ // Set the chrome flag to the same value as the legacy experiment.
339
+ Host.InspectorFrontendHost.InspectorFrontendHostInstance.setChromeFlag(hostExperiment.aboutFlag, value);
340
+ // The legacy experiment will be cleaned up by `cleanUpStaleExperiments`.
341
+ }
342
+ }
343
+
331
344
  #initializeExperiments(): void {
332
345
  Root.Runtime.experiments.register(
333
346
  Root.ExperimentNames.ExperimentName.CAPTURE_NODE_CREATION_STACKS, 'Capture node creation stacks');
@@ -413,7 +426,6 @@ export class MainImpl {
413
426
  if (enabledExperiments) {
414
427
  Root.Runtime.experiments.setServerEnabledExperiments(enabledExperiments.split(';'));
415
428
  }
416
- Root.Runtime.experiments.enableExperimentsTransiently([]);
417
429
 
418
430
  if (Host.InspectorFrontendHost.isUnderTest()) {
419
431
  const testParam = Root.Runtime.Runtime.queryParam('test');
@@ -57,7 +57,7 @@ Content:
57
57
  "function_declarations": [
58
58
  {
59
59
  "name": "getStyles",
60
- "description": "Get computed and source styles for one or multiple elements on the inspected page for multiple elements at once by uid.\n\n**CRITICAL** Use selectors to refer to elements in the text output. Do not use uids.\n**CRITICAL** Always provide the explanation argument to explain what and why you query.",
60
+ "description": "Get computed and source styles for one or multiple elements on the inspected page for multiple elements at once by uid.\n\n**CRITICAL** An element uid is a number, not a selector.\n**CRITICAL** Use selectors to refer to elements in the text output. Do not use uids.\n**CRITICAL** Always provide the explanation argument to explain what and why you query.",
61
61
  "parameters": {
62
62
  "type": 6,
63
63
  "description": "",
@@ -70,9 +70,9 @@ Content:
70
70
  },
71
71
  "elements": {
72
72
  "type": 5,
73
- "description": "A list of element uids to get data for",
73
+ "description": "A list of element uids to get data for. These are numbers, not selectors.",
74
74
  "items": {
75
- "type": 1,
75
+ "type": 3,
76
76
  "description": "An element uid."
77
77
  },
78
78
  "nullable": false
@@ -279,13 +279,14 @@ export class StylingAgent extends AiAgent<SDK.DOMModel.DOMNode> {
279
279
  });
280
280
 
281
281
  this.declareFunction<{
282
- elements: string[],
282
+ elements: number[],
283
283
  styleProperties: string[],
284
284
  explanation: string,
285
285
  }>('getStyles', {
286
286
  description:
287
287
  `Get computed and source styles for one or multiple elements on the inspected page for multiple elements at once by uid.
288
288
 
289
+ **CRITICAL** An element uid is a number, not a selector.
289
290
  **CRITICAL** Use selectors to refer to elements in the text output. Do not use uids.
290
291
  **CRITICAL** Always provide the explanation argument to explain what and why you query.`,
291
292
  parameters: {
@@ -300,8 +301,8 @@ export class StylingAgent extends AiAgent<SDK.DOMModel.DOMNode> {
300
301
  },
301
302
  elements: {
302
303
  type: Host.AidaClient.ParametersTypes.ARRAY,
303
- description: 'A list of element uids to get data for',
304
- items: {type: Host.AidaClient.ParametersTypes.STRING, description: `An element uid.`},
304
+ description: 'A list of element uids to get data for. These are numbers, not selectors.',
305
+ items: {type: Host.AidaClient.ParametersTypes.INTEGER, description: `An element uid.`},
305
306
  nullable: false,
306
307
  },
307
308
  styleProperties: {
@@ -605,7 +606,7 @@ const data = {
605
606
  return this.context?.getItem() ?? null;
606
607
  }
607
608
 
608
- async getStyles(elements: string[], properties: string[]): Promise<FunctionCallHandlerResult<unknown>> {
609
+ async getStyles(elements: number[], properties: string[]): Promise<FunctionCallHandlerResult<unknown>> {
609
610
  const result:
610
611
  Record<string, {computed: Record<string, string|undefined>, authored: Record<string, string|undefined>}> = {};
611
612
  for (const uid of elements) {
@@ -94,9 +94,6 @@ function getIssueCode(details: Protocol.Audits.CorsIssueDetails): IssueCode {
94
94
  case Protocol.Network.CorsError.LocalNetworkAccessPermissionDenied:
95
95
  return IssueCode.LOCAL_NETWORK_ACCESS_PERMISSION_DENIED;
96
96
  }
97
- // TODO(b/394636065): Remove this once browser protocol has rolled, as we
98
- // will never hit this case.
99
- return null as unknown as IssueCode;
100
97
  }
101
98
 
102
99
  export class CorsIssue extends Issue<Protocol.Audits.CorsIssueDetails, IssueCode> {
@@ -62,3 +62,43 @@ export const enum Events {
62
62
  export interface EventTypes {
63
63
  [Events.UPDATED]: void;
64
64
  }
65
+
66
+ /**
67
+ * A small wrapper around a DebuggableFrame usable as a UI.Context flavor.
68
+ * This is necessary as Frame and DebuggableFrame are updated in place, but
69
+ * for UI.Context we need a new instance.
70
+ */
71
+ export class DebuggableFrameFlavor implements DebuggableFrame {
72
+ static #last?: DebuggableFrameFlavor;
73
+
74
+ readonly url?: string;
75
+ readonly uiSourceCode?: Workspace.UISourceCode.UISourceCode;
76
+ readonly name?: string;
77
+ readonly line: number;
78
+ readonly column: number;
79
+ readonly missingDebugInfo?: MissingDebugInfo;
80
+ readonly sdkFrame: SDK.DebuggerModel.CallFrame;
81
+
82
+ private constructor(frame: DebuggableFrame) {
83
+ this.url = frame.url;
84
+ this.uiSourceCode = frame.uiSourceCode;
85
+ this.name = frame.name;
86
+ this.line = frame.line;
87
+ this.column = frame.column;
88
+ this.missingDebugInfo = frame.missingDebugInfo;
89
+ this.sdkFrame = frame.sdkFrame;
90
+ }
91
+
92
+ /** @returns the same instance of DebuggableFrameFlavor for repeated calls with the same (i.e. deep equal) DebuggableFrame */
93
+ static for(frame: DebuggableFrame): DebuggableFrameFlavor {
94
+ function equals(a: DebuggableFrame, b: DebuggableFrame): boolean {
95
+ return a.url === b.url && a.uiSourceCode === b.uiSourceCode && a.name === b.name && a.line === b.line &&
96
+ a.column === b.column && a.sdkFrame === b.sdkFrame;
97
+ }
98
+
99
+ if (!DebuggableFrameFlavor.#last || !equals(DebuggableFrameFlavor.#last, frame)) {
100
+ DebuggableFrameFlavor.#last = new DebuggableFrameFlavor(frame);
101
+ }
102
+ return DebuggableFrameFlavor.#last;
103
+ }
104
+ }
@@ -135,6 +135,8 @@ export interface ViewInput {
135
135
  onRemoveImageInput: () => void;
136
136
  onImageUpload: (ev: Event) => void;
137
137
  onImagePaste: (event: ClipboardEvent) => void;
138
+ onImageDragOver: (event: DragEvent) => void;
139
+ onImageDrop: (event: DragEvent) => void;
138
140
  }
139
141
 
140
142
  export type ViewOutput = undefined;
@@ -289,6 +291,8 @@ export const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLE
289
291
  maxlength="10000"
290
292
  @keydown=${input.onTextAreaKeyDown}
291
293
  @paste=${input.onImagePaste}
294
+ @dragover=${input.onImageDragOver}
295
+ @drop=${input.onImageDrop}
292
296
  @input=${(event: KeyboardEvent) => {
293
297
  input.onTextInputChange((event.target as HTMLInputElement).value);
294
298
  }}
@@ -550,12 +554,12 @@ export class ChatInput extends UI.Widget.Widget implements SDK.TargetManager.Obs
550
554
  });
551
555
  }
552
556
 
553
- #handleImagePaste = (event: ClipboardEvent): void => {
557
+ #handleImageDataTransferEvent(dataTransfer: DataTransfer|null, event: Event): void {
554
558
  if (this.conversationType !== AiAssistanceModel.AiHistoryStorage.ConversationType.STYLING) {
555
559
  return;
556
560
  }
557
561
 
558
- const files = event.clipboardData?.files;
562
+ const files = dataTransfer?.files;
559
563
  if (!files || files.length === 0) {
560
564
  return;
561
565
  }
@@ -567,6 +571,22 @@ export class ChatInput extends UI.Widget.Widget implements SDK.TargetManager.Obs
567
571
 
568
572
  event.preventDefault();
569
573
  void this.#handleLoadImage(imageFile);
574
+ }
575
+
576
+ #handleImagePaste = (event: ClipboardEvent): void => {
577
+ this.#handleImageDataTransferEvent(event.clipboardData, event);
578
+ };
579
+
580
+ #handleImageDragOver = (event: DragEvent): void => {
581
+ if (this.conversationType !== AiAssistanceModel.AiHistoryStorage.ConversationType.STYLING) {
582
+ return;
583
+ }
584
+
585
+ event.preventDefault();
586
+ };
587
+
588
+ #handleImageDrop = (event: DragEvent): void => {
589
+ this.#handleImageDataTransferEvent(event.dataTransfer, event);
570
590
  };
571
591
 
572
592
  async #handleLoadImage(file: File): Promise<void> {
@@ -663,6 +683,8 @@ export class ChatInput extends UI.Widget.Widget implements SDK.TargetManager.Obs
663
683
  onTextAreaKeyDown: this.onTextAreaKeyDown,
664
684
  onCancel: this.onCancel,
665
685
  onImageUpload: this.onImageUpload,
686
+ onImageDragOver: this.#handleImageDragOver,
687
+ onImageDrop: this.#handleImageDrop,
666
688
  },
667
689
  undefined, this.contentElement);
668
690
  }
@@ -19,33 +19,43 @@ const UIStringsNotTranslate = {
19
19
  */
20
20
  codeCompletionJustGotBetter: 'Code completion just got better',
21
21
  /**
22
- * @description First item in the description
23
- */
24
- asYouType: 'As you type, DevTools generates code suggestions to help you code faster.',
25
- /**
26
- * @description Second item in the description
22
+ * @description First item in the description.
27
23
  */
28
24
  describeCodeInComment:
29
- 'In Console and Sources, you can now describe the code you need in a comment, then press Ctrl+I to generate it.',
25
+ 'Pressing Ctrl+I on a comment in the Console and Sources panels now generates entire code blocks based on the instructions in the comment.',
30
26
  /**
31
- * @description Second item in the description
27
+ * @description First item in the description.
32
28
  */
33
29
  describeCodeInCommentForMacOs:
34
- 'In Console and Sources, you can now describe the code you need in a comment, then press Cmd+I to generate it.',
30
+ 'Pressing Cmd+I on a comment in the Console and Sources panels now generates entire code blocks based on the instructions in the comment.',
31
+ /**
32
+ * @description Second item in the description.
33
+ */
34
+ asYouType: 'You will still receive the real-time, as-you-type suggestions to help you code faster.',
35
+ /**
36
+ * @description Third item in the description.
37
+ */
38
+ disclaimerTextPrivacy:
39
+ 'To generate code suggestions, your console input, the history of your current console session, the currently inspected CSS, and the contents of the currently open file are shared with Google. This data may be seen by human reviewers to improve this feature.',
40
+ /**
41
+ * @description Third item in the description.
42
+ */
43
+ disclaimerTextPrivacyNoLogging:
44
+ 'To generate code suggestions, your console input, the history of your current console session, the currently inspected CSS, and the contents of the currently open file are shared with Google. This data will not be used to improve Google’s AI models. Your organization may change these settings at any time.',
35
45
  /**
36
46
  * @description Text for the manage in settings button in the upgrade notice dialog.
37
47
  */
38
48
  manageInSettings: 'Manage in settings',
39
49
  /**
40
- * @description Text for the got it button in the upgrade notice dialog.
50
+ * @description Text for the generate code button in the upgrade notice dialog.
41
51
  */
42
- gotIt: 'Got it',
52
+ generateCode: 'Generate code',
43
53
  } as const;
44
54
 
45
55
  const lockedString = i18n.i18n.lockedString;
46
56
 
47
57
  export class AiCodeGenerationUpgradeDialog {
48
- static show(): void {
58
+ static show({noLogging}: {noLogging: boolean}): void {
49
59
  const dialog = new UI.Dialog.Dialog();
50
60
  dialog.setAriaLabel(lockedString(UIStringsNotTranslate.codeCompletionJustGotBetter));
51
61
  // clang-format off
@@ -63,10 +73,6 @@ export class AiCodeGenerationUpgradeDialog {
63
73
  </h2>
64
74
  </header>
65
75
  <main class="reminder-container">
66
- <div class="reminder-item">
67
- <devtools-icon class="reminder-icon" name="code"></devtools-icon>
68
- <span>${lockedString(UIStringsNotTranslate.asYouType)}</span>
69
- </div>
70
76
  <div class="reminder-item">
71
77
  <devtools-icon class="reminder-icon" name="text-analysis"></devtools-icon>
72
78
  <span>
@@ -75,6 +81,16 @@ export class AiCodeGenerationUpgradeDialog {
75
81
  lockedString(UIStringsNotTranslate.describeCodeInComment)}
76
82
  </span>
77
83
  </div>
84
+ <div class="reminder-item">
85
+ <devtools-icon class="reminder-icon" name="code"></devtools-icon>
86
+ <span>${lockedString(UIStringsNotTranslate.asYouType)}</span>
87
+ </div>
88
+ <div class="reminder-item">
89
+ <devtools-icon class="reminder-icon" name="google"></devtools-icon>
90
+ <span>${noLogging ? lockedString(UIStringsNotTranslate.disclaimerTextPrivacyNoLogging) :
91
+ lockedString(UIStringsNotTranslate.disclaimerTextPrivacy)}
92
+ </span>
93
+ </div>
78
94
  </main>
79
95
  <footer>
80
96
  <div class="right-buttons">
@@ -93,7 +109,7 @@ export class AiCodeGenerationUpgradeDialog {
93
109
  }}
94
110
  jslogcontext="ai-code-generation-upgrade-dialog.continue"
95
111
  .variant=${Buttons.Button.Variant.PRIMARY}>
96
- ${lockedString(UIStringsNotTranslate.gotIt)}
112
+ ${lockedString(UIStringsNotTranslate.generateCode)}
97
113
  </devtools-button>
98
114
  </div>
99
115
  </footer>
@@ -199,11 +199,34 @@ export const format = (fmt: string, args: SDK.RemoteObject.RemoteObject[]): {
199
199
  return {tokens, args: args.slice(argIndex)};
200
200
  };
201
201
 
202
+ /**
203
+ * This function converts a string into a partial regex string that
204
+ * case-insensitively matches it in CSS, even if CSS escapes are used.
205
+ *
206
+ * @param cssString the target string.
207
+ * @returns a partial regex matching the string in CSS.
208
+ */
209
+ const cssEscapeRegex = (cssString: string): string => {
210
+ return [...cssString]
211
+ .map(char => {
212
+ const charCodes = new Set([char.toLowerCase(), char.toUpperCase()].map(c => c.charCodeAt(0).toString(16)));
213
+ const charCodeRegex =
214
+ [...charCodes].map(charCode => `\\\\0{0,${6 - charCode.length}}${charCode}[ \\n\\t]?`).join('|');
215
+ return `\\\\?(?:${charCodeRegex}|${char})`;
216
+ })
217
+ .join('');
218
+ };
219
+
202
220
  export const updateStyle = (currentStyle: Map<string, {value: string, priority: string}>, styleToAdd: string): void => {
203
221
  const ALLOWED_PROPERTY_PREFIXES = ['background', 'border', 'color', 'font', 'line', 'margin', 'padding', 'text'];
204
222
  // We only allow data URLs with the `url()` CSS function.
205
223
  // The capture group is not intended to grab the whole URL exactly, just enough so we can check the scheme.
206
- const URL_REGEX = /url\([\'\"]?([^\)]*)/g;
224
+ // The regex also covers CSS hex-escaped variations of `url()`.
225
+ const URL_REGEX = new RegExp(`(?=${cssEscapeRegex('url')}\\(['"]?([^\\)]*))`, 'gi');
226
+ // We greedily capture all `image-set()`s to make sure that all of
227
+ // them properly use `url()`s to enforce the data URL check later.
228
+ const IMAGESET_REGEX = new RegExp(`(?=(${cssEscapeRegex('image-set')}\\(.*))`, 'gi');
229
+ const GOOD_IMAGESET_REGEX = /^image-set\((?:(?:(?:url|type)\("[^\\"]*"\)|[\d.]+(?:x|dpi|dpcm|dppx)),?\s*)+\)/i;
207
230
 
208
231
  currentStyle.clear();
209
232
  /* eslint-disable-next-line @devtools/no-imperative-dom-api --
@@ -218,9 +241,15 @@ export const updateStyle = (currentStyle: Map<string, {value: string, priority:
218
241
  continue;
219
242
  }
220
243
 
244
+ const value = buffer.style.getPropertyValue(property);
245
+ // We make sure every `image-set()` only uses `url()`s for its images.
246
+ // If any of them seem malformed, we skip the whole property.
247
+ const imageSets = [...value.matchAll(IMAGESET_REGEX)];
248
+ if (imageSets.some(match => !GOOD_IMAGESET_REGEX.test(match[1]))) {
249
+ continue;
250
+ }
221
251
  // There could be multiple `url()` functions, so we check them all.
222
252
  // If any of them is not a `data` URL, we skip the whole property.
223
- const value = buffer.style.getPropertyValue(property);
224
253
  const potentialUrls = [...value.matchAll(URL_REGEX)].map(match => match[1]);
225
254
  if (potentialUrls.some(
226
255
  potentialUrl => !Common.ParsedURL.schemeIs(potentialUrl as Platform.DevToolsPath.UrlString, 'data:'))) {
@@ -626,6 +626,7 @@ export class ConsoleInsightTeaser extends UI.Widget.Widget {
626
626
  this.#startTime = performance.now();
627
627
  let teaserText = '';
628
628
  let firstChunkReceived = false;
629
+ let firstChunkTime = 0;
629
630
  try {
630
631
  for await (const chunk of this.#getOnDeviceInsight()) {
631
632
  teaserText += chunk;
@@ -634,7 +635,9 @@ export class ConsoleInsightTeaser extends UI.Widget.Widget {
634
635
  this.requestUpdate();
635
636
  if (!firstChunkReceived) {
636
637
  firstChunkReceived = true;
637
- Host.userMetrics.consoleInsightTeaserFirstChunkGenerated(performance.now() - this.#startTime);
638
+ firstChunkTime = performance.now();
639
+ Host.userMetrics.consoleInsightTeaserFirstChunkGenerated(firstChunkTime - this.#startTime);
640
+ Host.userMetrics.consoleInsightTeaserFirstChunkGeneratedMedium(firstChunkTime - this.#startTime);
638
641
  }
639
642
  }
640
643
  } catch (err) {
@@ -654,6 +657,8 @@ export class ConsoleInsightTeaser extends UI.Widget.Widget {
654
657
  clearTimeout(this.#timeoutId);
655
658
  const duration = performance.now() - this.#startTime;
656
659
  Host.userMetrics.consoleInsightTeaserGenerated(duration);
660
+ Host.userMetrics.consoleInsightTeaserGeneratedMedium(duration);
661
+ Host.userMetrics.consoleInsightTeaserChunkToEndMedium(performance.now() - firstChunkTime);
657
662
  if (teaserText.length > 300) {
658
663
  Host.userMetrics.consoleInsightLongTeaserGenerated(duration);
659
664
  } else {
@@ -1092,6 +1092,7 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
1092
1092
  }
1093
1093
 
1094
1094
  override onbind(): void {
1095
+ this.performUpdate();
1095
1096
  if (this.treeOutline && !this.isClosingTag()) {
1096
1097
  this.treeOutline.treeElementByNode.set(this.nodeInternal, this);
1097
1098
  this.nodeInternal.addEventListener(SDK.DOMModel.DOMNodeEvents.TOP_LAYER_INDEX_CHANGED, this.performUpdate, this);
@@ -1115,6 +1116,49 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
1115
1116
  if (this.editing) {
1116
1117
  this.editing.cancel();
1117
1118
  }
1119
+ // Update the element to clean up adorner registrations with the
1120
+ // ElementsPanel.
1121
+ // We do not change the ElementsTreeElement state in case the
1122
+ // element is bound again.
1123
+ DEFAULT_VIEW(
1124
+ {
1125
+ containerAdornerActive: false,
1126
+ showAdAdorner: false,
1127
+ showContainerAdorner: false,
1128
+ containerType: this.#layout?.containerType,
1129
+ showFlexAdorner: false,
1130
+ flexAdornerActive: false,
1131
+ showGridAdorner: false,
1132
+ showGridLanesAdorner: false,
1133
+ showMediaAdorner: false,
1134
+ showPopoverAdorner: false,
1135
+ showTopLayerAdorner: false,
1136
+ gridAdornerActive: false,
1137
+ popoverAdornerActive: false,
1138
+ isSubgrid: false,
1139
+ showViewSourceAdorner: false,
1140
+ showScrollAdorner: false,
1141
+ showScrollSnapAdorner: false,
1142
+ scrollSnapAdornerActive: false,
1143
+ showSlotAdorner: false,
1144
+ showStartingStyleAdorner: false,
1145
+ startingStyleAdornerActive: false,
1146
+ nodeInfo: this.#nodeInfo,
1147
+ onStartingStyleAdornerClick: () => {},
1148
+ onSlotAdornerClick: () => {},
1149
+ topLayerIndex: -1,
1150
+ onViewSourceAdornerClick: () => {},
1151
+ onGutterClick: () => {},
1152
+ onContainerAdornerClick: () => {},
1153
+ onFlexAdornerClick: () => {},
1154
+ onGridAdornerClick: () => {},
1155
+ onMediaAdornerClick: () => {},
1156
+ onPopoverAdornerClick: () => {},
1157
+ onScrollSnapAdornerClick: () => {},
1158
+ onTopLayerAdornerClick: () => {},
1159
+ },
1160
+ this, this.listItemElement);
1161
+
1118
1162
  if (this.treeOutline && this.treeOutline.treeElementByNode.get(this.nodeInternal) === this) {
1119
1163
  this.treeOutline.treeElementByNode.delete(this.nodeInternal);
1120
1164
  }
@@ -80,7 +80,7 @@ const str_ = i18n.i18n.registerUIStrings('panels/elements/ElementsTreeOutline.ts
80
80
  const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
81
81
  const elementsTreeOutlineByDOMModel = new WeakMap<SDK.DOMModel.DOMModel, ElementsTreeOutline>();
82
82
 
83
- const populatedTreeElements = new Set<ElementsTreeElement>();
83
+ const populatedTreeElements = new WeakSet<ElementsTreeElement>();
84
84
 
85
85
  export type View = typeof DEFAULT_VIEW;
86
86
 
@@ -383,7 +383,7 @@ export class GenericSettingsTab extends UI.Widget.VBox implements SettingsTab {
383
383
 
384
384
  export class ExperimentsSettingsTab extends UI.Widget.VBox implements SettingsTab {
385
385
  #experimentsSection: Card|undefined;
386
- private readonly experimentToControl = new Map<Root.Runtime.Experiment, HTMLElement>();
386
+ private readonly experimentToControl = new Map<Root.Runtime.Experiment|Root.Runtime.HostExperiment, HTMLElement>();
387
387
  private readonly containerElement: HTMLElement;
388
388
 
389
389
  constructor() {
@@ -452,12 +452,16 @@ export class ExperimentsSettingsTab extends UI.Widget.VBox implements SettingsTa
452
452
  return subsection;
453
453
  }
454
454
 
455
- private createExperimentCheckbox(experiment: Root.Runtime.Experiment): HTMLParagraphElement {
455
+ private createExperimentCheckbox(experiment: Root.Runtime.Experiment|Root.Runtime.HostExperiment):
456
+ HTMLParagraphElement {
456
457
  const checkbox =
457
458
  UI.UIUtils.CheckboxLabel.createWithStringLiteral(experiment.title, experiment.isEnabled(), experiment.name);
458
459
  checkbox.classList.add('experiment-label');
459
460
  checkbox.name = experiment.name;
460
461
  function listener(): void {
462
+ if (experiment instanceof Root.Runtime.HostExperiment) {
463
+ Host.InspectorFrontendHost.InspectorFrontendHostInstance.setChromeFlag(experiment.aboutFlag, checkbox.checked);
464
+ }
461
465
  experiment.setEnabled(checkbox.checked);
462
466
  Host.userMetrics.experimentChanged(experiment.name, experiment.isEnabled());
463
467
  UI.InspectorView.InspectorView.instance().displayReloadRequiredWarning(
@@ -499,7 +503,7 @@ export class ExperimentsSettingsTab extends UI.Widget.VBox implements SettingsTa
499
503
  }
500
504
 
501
505
  highlightObject(experiment: Object): void {
502
- if (experiment instanceof Root.Runtime.Experiment) {
506
+ if (experiment instanceof Root.Runtime.Experiment || experiment instanceof Root.Runtime.HostExperiment) {
503
507
  const element = this.experimentToControl.get(experiment);
504
508
  if (element) {
505
509
  PanelUtils.highlightElement(element);
@@ -534,10 +538,12 @@ export class ActionDelegate implements UI.ActionRegistration.ActionDelegate {
534
538
  return false;
535
539
  }
536
540
  }
537
- export class Revealer implements Common.Revealer.Revealer<Root.Runtime.Experiment|Common.Settings.Setting<unknown>> {
538
- async reveal(object: Root.Runtime.Experiment|Common.Settings.Setting<unknown>): Promise<void> {
541
+ export class Revealer implements
542
+ Common.Revealer.Revealer<Root.Runtime.Experiment|Root.Runtime.HostExperiment|Common.Settings.Setting<unknown>> {
543
+ async reveal(object: Root.Runtime.Experiment|Root.Runtime.HostExperiment|Common.Settings.Setting<unknown>):
544
+ Promise<void> {
539
545
  const context = UI.Context.Context.instance();
540
- if (object instanceof Root.Runtime.Experiment) {
546
+ if (object instanceof Root.Runtime.Experiment || object instanceof Root.Runtime.HostExperiment) {
541
547
  Host.InspectorFrontendHost.InspectorFrontendHostInstance.bringToFront();
542
548
  await SettingsScreen.showSettingsScreen({name: 'experiments'});
543
549
  const experimentsSettingsTab = context.flavor(ExperimentsSettingsTab);
@@ -277,6 +277,7 @@ Common.Revealer.registerRevealer({
277
277
  return [
278
278
  Common.Settings.Setting,
279
279
  Root.Runtime.Experiment,
280
+ Root.Runtime.HostExperiment,
280
281
  ];
281
282
  },
282
283
  destination: undefined,
@@ -40,28 +40,28 @@ export function getReleaseNote(): ReleaseNote {
40
40
  }
41
41
 
42
42
  let releaseNote: ReleaseNote = {
43
- version: 144,
44
- header: 'What\'s new in DevTools 144',
43
+ version: 145,
44
+ header: 'What\'s new in DevTools 145',
45
45
  markdownLinks: [
46
46
  {
47
- key: 'request-conditions',
48
- link: 'https://developer.chrome.com/blog/new-in-devtools-144/#request-conditions',
47
+ key: 'soft-navigations',
48
+ link: 'https://developer.chrome.com/blog/new-in-devtools-145/#soft-navigations',
49
49
  },
50
50
  {
51
- key: 'mcp-server',
52
- link: 'https://developer.chrome.com/blog/new-in-devtools-144/#mcp-server',
51
+ key: 'render-blocking',
52
+ link: 'https://developer.chrome.com/blog/new-in-devtools-145/#render-blocking',
53
53
  },
54
54
  {
55
- key: 'adopted-stylesheets',
56
- link: 'https://developer.chrome.com/blog/new-in-devtools-144/#adopted-stylesheets',
55
+ key: 'mcp-server',
56
+ link: 'https://developer.chrome.com/blog/new-in-devtools-145/#mcp-server',
57
57
  },
58
58
  ],
59
59
  videoLinks: [
60
60
  {
61
- description: 'See past highlights from Chrome 144',
62
- link: 'https://developer.chrome.com/blog/new-in-devtools-144' as Platform.DevToolsPath.UrlString,
61
+ description: 'See past highlights from Chrome 142-144',
62
+ link: 'https://www.youtube.com/watch?v=2rOeZ98AOb8' as Platform.DevToolsPath.UrlString,
63
63
  type: VideoType.WHATS_NEW,
64
64
  },
65
65
  ],
66
- link: 'https://developer.chrome.com/blog/new-in-devtools-144/',
66
+ link: 'https://developer.chrome.com/blog/new-in-devtools-145/',
67
67
  };
@@ -1,11 +1,11 @@
1
- ### [Request conditions](request-conditions)
1
+ ### [Soft navigations in performance traces](soft-navigations)
2
2
 
3
- Block and throttle individual network requests with the new Request conditions panel.
3
+ Soft navigation and soft LCP markers are now visible in performance traces for single-page applications.
4
4
 
5
- ### [MCP server](mcp-server)
5
+ ### [Identify render blocking requests](render-blocking)
6
6
 
7
- Use auto connect to continue a debugging session in an already running Chrome instance.
7
+ Enable the Render blocking column in the Network panel to spot scripts or stylesheets that are blocking the rendering process.
8
8
 
9
- ### [Adopted stylesheets](adopted-stylesheets)
9
+ ### [MCP server](mcp-server)
10
10
 
11
- Adopted stylesheets are now visible under shadow roots in the Elements panel.
11
+ Simulate device viewports and user agents, work with extensions, and open pages in the background.
@@ -180,6 +180,9 @@ export class McpHostBindings implements Host.InspectorFrontendHostAPI.InspectorF
180
180
  recordPerformanceHistogram(): void {
181
181
  }
182
182
 
183
+ recordPerformanceHistogramMedium(): void {
184
+ }
185
+
183
186
  recordUserMetricsAction(): void {
184
187
  }
185
188
 
@@ -307,4 +310,7 @@ export class McpHostBindings implements Host.InspectorFrontendHostAPI.InspectorF
307
310
 
308
311
  recordFunctionCall(): void {
309
312
  }
313
+
314
+ setChromeFlag(): void {
315
+ }
310
316
  }
package/package.json CHANGED
@@ -105,5 +105,5 @@
105
105
  "flat-cache": "6.1.12"
106
106
  }
107
107
  },
108
- "version": "1.0.1574367"
108
+ "version": "1.0.1575174"
109
109
  }