chrome-devtools-frontend 1.0.1587572 → 1.0.1587905

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.
@@ -25,6 +25,10 @@ const scopeTrees = new WeakMap<Script, Promise<{scopeTree: ScopeTreeNode, text:
25
25
  * and the text allows conversion from/to line/column numbers.
26
26
  */
27
27
  export function scopeTreeForScript(script: Script): Promise<{scopeTree: ScopeTreeNode, text: Text}|null> {
28
+ if (script.isWasm()) {
29
+ return Promise.resolve(null);
30
+ }
31
+
28
32
  let promise = scopeTrees.get(script);
29
33
  if (promise === undefined) {
30
34
  promise = script.requestContentData().then(content => {
@@ -94,6 +94,18 @@ export const UIStrings = {
94
94
  * @description Warning displayed to developers that instead of calling the `Intl.v8BreakIterator` constructor, which is not a standard JavaScript API, use ECMA402 standard API Intl.Segmenter shipped in end of 2020 instead.
95
95
  */
96
96
  IntlV8BreakIterator: "`Intl.v8BreakIterator` is deprecated. Please use `Intl.Segmenter` instead.",
97
+ /**
98
+ * @description Warning for using deprecated 'inputQuota' attribute.
99
+ */
100
+ LanguageModel_InputQuota: "LanguageModel.inputQuota is deprecated. Please use LanguageModel.contextWindow instead. This alias is only available in extensions.",
101
+ /**
102
+ * @description Warning for using deprecated 'inputUsage' attribute.
103
+ */
104
+ LanguageModel_InputUsage: "LanguageModel.inputUsage is deprecated. Please use LanguageModel.contextUsage instead. This alias is only available in extensions.",
105
+ /**
106
+ * @description Warning for using deprecated 'measureInputUsage' method.
107
+ */
108
+ LanguageModel_MeasureInputUsage: "LanguageModel.measureInputUsage() is deprecated. Please use LanguageModel.measureContextUsage() instead. This alias is only available in extensions.",
97
109
  /**
98
110
  * @description Warning message for web developers when they call the deprecated LanguageModel.params() method.
99
111
  */
@@ -313,6 +325,15 @@ export const DEPRECATIONS_METADATA: Partial<Record<string, DeprecationDescriptor
313
325
  "LanguageModelTopK": {
314
326
  "chromeStatusFeature": 5134603979063296
315
327
  },
328
+ "LanguageModel_InputQuota": {
329
+ "chromeStatusFeature": 5134603979063296
330
+ },
331
+ "LanguageModel_InputUsage": {
332
+ "chromeStatusFeature": 5134603979063296
333
+ },
334
+ "LanguageModel_MeasureInputUsage": {
335
+ "chromeStatusFeature": 5134603979063296
336
+ },
316
337
  "LocalCSSFileExtensionRejected": {
317
338
  "milestone": 64
318
339
  },
@@ -425,6 +425,10 @@ Time: ${micros(time)}`;
425
425
  yield* this.#runAgent(initialQuery, options);
426
426
  }
427
427
 
428
+ #getQueryAfterSelection(initialQuery: string, selection: string): string {
429
+ return `${selection}\nOriginal user query: ${initialQuery}`;
430
+ }
431
+
428
432
  async *
429
433
  #runAgent(
430
434
  initialQuery: string,
@@ -467,7 +471,7 @@ Time: ${micros(time)}`;
467
471
  // requery with the specialized agent.
468
472
  if (data.type === ResponseType.CONTEXT_CHANGE) {
469
473
  this.setContext(data.context);
470
- yield* this.#runAgent(initialQuery, options);
474
+ yield* this.#runAgent(this.#getQueryAfterSelection(initialQuery, data.description), options);
471
475
  return;
472
476
  }
473
477
  }
@@ -88,6 +88,12 @@ export interface SideEffectResponse {
88
88
  }
89
89
  export interface ContextChangeResponse {
90
90
  type: ResponseType.CONTEXT_CHANGE;
91
+ /**
92
+ * Information to pass down what was selected
93
+ * Use to make the LLM understand the the user
94
+ * already selected something.
95
+ */
96
+ description: string;
91
97
  context: ConversationContext<unknown>;
92
98
  }
93
99
 
@@ -212,7 +218,8 @@ export type FunctionCallHandlerResult<Result> = {
212
218
  }|{
213
219
  result: Result,
214
220
  }|{
215
- context: unknown,
221
+ context: ConversationContext<unknown>,
222
+ description: string,
216
223
  }|{
217
224
  error: string,
218
225
  };
@@ -484,7 +491,9 @@ export abstract class AiAgent<T> {
484
491
  * called with one object with `foo` and `bar` keys.
485
492
  */
486
493
  protected declareFunction<Args extends Record<string, unknown>, ReturnType = unknown>(
487
- name: string, declaration: FunctionDeclaration<Args, ReturnType>): void {
494
+ name: string,
495
+ declaration: FunctionDeclaration<Args, ReturnType>,
496
+ ): void {
488
497
  if (this.#functionDeclarations.has(name)) {
489
498
  throw new Error(`Duplicate function declaration ${name}`);
490
499
  }
@@ -609,6 +618,7 @@ export abstract class AiAgent<T> {
609
618
  if ('context' in result) {
610
619
  yield {
611
620
  type: ResponseType.CONTEXT_CHANGE,
621
+ description: result.description,
612
622
  context: result.context,
613
623
  };
614
624
 
@@ -643,7 +653,9 @@ export abstract class AiAgent<T> {
643
653
  name: string,
644
654
  args: Record<string, unknown>,
645
655
  options?: FunctionHandlerOptions&{explanation?: string},
646
- ): AsyncGenerator<FunctionCallResponseData, {result: unknown}|{context: ConversationContext<unknown>}> {
656
+ ): AsyncGenerator<FunctionCallResponseData, {
657
+ result: unknown,
658
+ }|{context: ConversationContext<unknown>, description: string}> {
647
659
  const call = this.#functionDeclarations.get(name);
648
660
  if (!call) {
649
661
  throw new Error(`Function ${name} is not found.`);
@@ -755,7 +767,7 @@ export abstract class AiAgent<T> {
755
767
  }
756
768
 
757
769
  if ('context' in result) {
758
- return result as {context: ConversationContext<unknown>};
770
+ return result;
759
771
  }
760
772
 
761
773
  return result as {result: unknown};
@@ -169,6 +169,7 @@ export class ContextSelectionAgent extends AiAgent<never> {
169
169
  const calculator = this.#networkTimeCalculator ?? new NetworkTimeCalculator.NetworkTransferTimeCalculator();
170
170
  return {
171
171
  context: new RequestContext(request, calculator),
172
+ description: 'User selected a network request',
172
173
  };
173
174
  }
174
175
 
@@ -231,11 +232,14 @@ export class ContextSelectionAgent extends AiAgent<never> {
231
232
  if (file.fullDisplayName() === params.name) {
232
233
  return {
233
234
  context: new FileContext(file),
235
+ description: 'User selected a source file',
234
236
  };
235
237
  }
236
238
  }
237
239
 
238
- return {error: 'Unable to find file.'};
240
+ return {
241
+ error: 'Unable to find file.',
242
+ };
239
243
  },
240
244
  });
241
245
 
@@ -264,6 +268,7 @@ export class ContextSelectionAgent extends AiAgent<never> {
264
268
 
265
269
  return {
266
270
  context: PerformanceTraceContext.fromParsedTrace(result),
271
+ description: 'User recorded a performance trace',
267
272
  };
268
273
  }
269
274
  });
@@ -292,6 +297,7 @@ export class ContextSelectionAgent extends AiAgent<never> {
292
297
  if (node) {
293
298
  return {
294
299
  context: new NodeContext(node),
300
+ description: 'User selected an element',
295
301
  };
296
302
  }
297
303
  return {
@@ -1426,8 +1426,6 @@ export class AiAssistancePanel extends UI.Panel.Panel {
1426
1426
  // Cancel any previous in-flight conversation.
1427
1427
  this.#cancel();
1428
1428
  const signal = this.#runAbortController.signal;
1429
- const context = this.#getConversationContext(this.#conversation.type);
1430
- this.#conversation.setContext(context);
1431
1429
 
1432
1430
  // If a different context is provided, it must be from the same origin.
1433
1431
  if (this.#conversation.isBlockedByOrigin) {
@@ -202,7 +202,8 @@ export const DEFAULT_VIEW = (input: ViewInput, _output: ViewOutput, target: HTML
202
202
  jslog=${VisualLogging.link('open-ai-settings').track({
203
203
  click: true,
204
204
  })}
205
- @click=${() => {
205
+ @click=${(ev: Event) => {
206
+ ev.preventDefault();
206
207
  void UI.ViewManager.ViewManager.instance().showView('chrome-ai');
207
208
  }}
208
209
  >${lockedString('Relevant data')}</button>&nbsp;${lockedString('is sent to Google')}
@@ -320,42 +321,46 @@ export const DEFAULT_VIEW = (input: ViewInput, _output: ViewOutput, target: HTML
320
321
  ></devtools-button>`
321
322
  : Lit.nothing}
322
323
  <div
323
- role=button
324
324
  class=${Lit.Directives.classMap({
325
325
  'resource-link': true,
326
326
  'has-picker-behavior': input.conversationType === AiAssistanceModel.AiHistoryStorage.ConversationType.STYLING,
327
- disabled: input.isTextInputDisabled,
328
327
  })}
329
- tabindex=${(input.conversationType === AiAssistanceModel.AiHistoryStorage.ConversationType.STYLING || input.isTextInputDisabled) ? '-1' : '0'}
330
- @click=${input.onContextClick}
331
- @keydown=${(ev: KeyboardEvent) => {
332
- if (ev.key === 'Enter' || ev.key === ' ') {
333
- void input.onContextClick();
334
- }
335
- }}
336
- aria-description=${i18nString(UIStrings.revealContextDescription)}
337
328
  >
338
- ${input.selectedContext instanceof AiAssistanceModel.NetworkAgent.RequestContext ?
339
- PanelUtils.PanelUtils.getIconForNetworkRequest(input.selectedContext.getItem()) :
340
- input.selectedContext instanceof AiAssistanceModel.FileAgent.FileContext ?
341
- PanelUtils.PanelUtils.getIconForSourceFile(input.selectedContext.getItem()) :
342
- input.selectedContext instanceof AiAssistanceModel.PerformanceAgent.PerformanceTraceContext ?
343
- html`<devtools-icon name="performance" title="Performance"></devtools-icon>` :
344
- Lit.nothing}
345
- <span class="title">
346
- ${input.selectedContext instanceof AiAssistanceModel.StylingAgent.NodeContext ?
329
+ ${
330
+ input.selectedContext instanceof AiAssistanceModel.StylingAgent.NodeContext ?
347
331
  html`
348
- <devtools-widget .widgetConfig=${UI.Widget.widgetConfig(PanelsCommon.DOMLinkifier.DOMNodeLink, {
349
- node: input.selectedContext.getItem(),
350
- options: {
351
- hiddenClassList: input.selectedContext.getItem().classNames().filter(
352
- className => className.startsWith(AiAssistanceModel.Injected.AI_ASSISTANCE_CSS_CLASS_NAME)),
353
- disabled: input.isTextInputDisabled,
354
- },
355
- })}></devtools-widget>`
356
- :
357
- input.selectedContext.getTitle()}
358
- </span>
332
+ <devtools-widget
333
+ class="title"
334
+ .widgetConfig=${UI.Widget.widgetConfig(PanelsCommon.DOMLinkifier.DOMNodeLink, {
335
+ node: input.selectedContext.getItem(),
336
+ options: {
337
+ hiddenClassList: input.selectedContext.getItem().classNames().filter(
338
+ className => className.startsWith(AiAssistanceModel.Injected.AI_ASSISTANCE_CSS_CLASS_NAME)),
339
+ ariaDescription: i18nString(UIStrings.revealContextDescription),
340
+ },
341
+ })}
342
+ ></devtools-widget>` :
343
+ html`
344
+ ${input.selectedContext instanceof AiAssistanceModel.NetworkAgent.RequestContext ?
345
+ PanelUtils.PanelUtils.getIconForNetworkRequest(input.selectedContext.getItem()) :
346
+ input.selectedContext instanceof AiAssistanceModel.FileAgent.FileContext ?
347
+ PanelUtils.PanelUtils.getIconForSourceFile(input.selectedContext.getItem()) :
348
+ input.selectedContext instanceof AiAssistanceModel.PerformanceAgent.PerformanceTraceContext ?
349
+ html`<devtools-icon name="performance" title="Performance"></devtools-icon>` :
350
+ Lit.nothing}
351
+ <span
352
+ role="button"
353
+ class="title"
354
+ tabindex="0"
355
+ @click=${input.onContextClick}
356
+ @keydown=${(ev: KeyboardEvent) => {
357
+ if (ev.key === 'Enter' || ev.key === ' ') {
358
+ void input.onContextClick();
359
+ }
360
+ }}
361
+ aria-description=${i18nString(UIStrings.revealContextDescription)}
362
+ >${input.selectedContext.getTitle()}</span>`
363
+ }
359
364
  ${input.onContextRemoved ? html`
360
365
  <devtools-button
361
366
  title=${getContextRemoveLabel(input.selectedContext)}
@@ -218,9 +218,9 @@
218
218
  gap: var(--sys-size-3);
219
219
  align-items: center;
220
220
 
221
- .resource-link,
222
- .resource-task {
221
+ .resource-link {
223
222
  display: flex;
223
+ background-color: var(--sys-color-cdt-base-container);
224
224
  align-items: center;
225
225
  cursor: pointer;
226
226
  padding: var(--sys-size-2) var(--sys-size-3);
@@ -250,6 +250,10 @@
250
250
 
251
251
  &.has-picker-behavior {
252
252
  overflow: visible;
253
+
254
+ .title {
255
+ overflow: visible;
256
+ }
253
257
  }
254
258
 
255
259
  &:focus-visible {
@@ -301,13 +305,6 @@
301
305
  }
302
306
  }
303
307
  }
304
-
305
- .resource-link.disabled,
306
- .resource-task.disabled {
307
- color: var(--sys-color-state-disabled);
308
- border-color: var(--sys-color-neutral-outline);
309
- pointer-events: none;
310
- }
311
308
  }
312
309
 
313
310
  .link {
@@ -11,7 +11,6 @@ import * as Platform from '../../core/platform/platform.js';
11
11
  import * as SDK from '../../core/sdk/sdk.js';
12
12
  import type * as Protocol from '../../generated/protocol.js';
13
13
  import * as TextUtils from '../../models/text_utils/text_utils.js';
14
- import * as LegacyWrapper from '../../ui/components/legacy_wrapper/legacy_wrapper.js';
15
14
  import * as DataGrid from '../../ui/legacy/components/data_grid/data_grid.js';
16
15
  import * as UI from '../../ui/legacy/legacy.js';
17
16
  import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
@@ -541,10 +540,9 @@ export class RequestView extends UI.Widget.VBox {
541
540
  this.resourceViewTabSetting =
542
541
  Common.Settings.Settings.instance().createSetting('cache-storage-view-tab', 'preview');
543
542
 
544
- this.tabbedPane.appendTab(
545
- 'headers', i18nString(UIStrings.headers),
546
- LegacyWrapper.LegacyWrapper.legacyWrapper(
547
- UI.Widget.VBox, new Network.RequestHeadersView.RequestHeadersView(request)));
543
+ const requestHeadersView = new Network.RequestHeadersView.RequestHeadersView();
544
+ requestHeadersView.request = request;
545
+ this.tabbedPane.appendTab('headers', i18nString(UIStrings.headers), requestHeadersView);
548
546
  this.tabbedPane.appendTab(
549
547
  'preview', i18nString(UIStrings.preview), new Network.RequestPreviewView.RequestPreviewView(request));
550
548
  this.tabbedPane.show(this.element);
@@ -150,12 +150,20 @@ export const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLE
150
150
  const shouldExpandCategory = (breakpoints: SDK.CategorizedBreakpoint.CategorizedBreakpoint[]): boolean =>
151
151
  Boolean(input.filterText) || (input.highlightedItem && breakpoints.includes(input.highlightedItem)) ||
152
152
  breakpoints.some(breakpoint => breakpoint.enabled());
153
- const filter = (breakpoint: SDK.CategorizedBreakpoint.CategorizedBreakpoint): boolean => !input.filterText ||
154
- Boolean(Sources.CategorizedBreakpointL10n.getLocalizedBreakpointName(breakpoint.name).match(input.filterText)) ||
153
+ const filterRegex =
154
+ input.filterText ? new RegExp(Platform.StringUtilities.escapeForRegExp(input.filterText), 'i') : null;
155
+ const filter = (breakpoint: SDK.CategorizedBreakpoint.CategorizedBreakpoint): boolean => !filterRegex ||
156
+ Boolean(Sources.CategorizedBreakpointL10n.getLocalizedBreakpointName(breakpoint.name).match(filterRegex)) ||
155
157
  breakpoint === input.highlightedItem;
156
158
  const filteredCategories =
157
159
  input.sortedCategoryNames.values()
158
- .map(category => [category, input.categories.get(category)?.filter(filter)])
160
+ .map(category => {
161
+ const breakpoints = input.categories.get(category);
162
+ if (filterRegex && getLocalizedCategory(category).match(filterRegex)) {
163
+ return [category, breakpoints];
164
+ }
165
+ return [category, breakpoints?.filter(filter)];
166
+ })
159
167
  .filter(
160
168
  (filteredCategory): filteredCategory is
161
169
  [SDK.CategorizedBreakpoint.Category, SDK.CategorizedBreakpoint.CategorizedBreakpoint[]] =>
@@ -12,7 +12,7 @@ import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
12
12
 
13
13
  import domLinkifierStyles from './domLinkifier.css.js';
14
14
 
15
- const {classMap} = Directives;
15
+ const {classMap, ifDefined} = Directives;
16
16
 
17
17
  const UIStrings = {
18
18
  /**
@@ -33,6 +33,7 @@ export interface Options {
33
33
  isDynamicLink?: boolean;
34
34
  hiddenClassList?: string[];
35
35
  disabled?: boolean;
36
+ ariaDescription?: string;
36
37
  }
37
38
 
38
39
  interface ViewInput {
@@ -43,6 +44,7 @@ interface ViewInput {
43
44
  id?: string;
44
45
  classes: string[];
45
46
  pseudo?: string;
47
+ ariaDescription?: string;
46
48
  onClick: () => void;
47
49
  onMouseOver: () => void;
48
50
  onMouseLeave: () => void;
@@ -59,6 +61,7 @@ const DEFAULT_VIEW: View = (input, _output, target: HTMLElement) => {
59
61
  'dynamic-link': Boolean(input.dynamic),
60
62
  disabled: Boolean(input.disabled)
61
63
  })}"
64
+ aria-description=${ifDefined(input.ariaDescription)}
62
65
  jslog=${VisualLogging.link('node').track({click: true, keydown: 'Enter'})}
63
66
  tabindex=${input.preventKeyboardFocus ? -1 : 0}
64
67
  @click=${input.onClick}
@@ -112,6 +115,7 @@ export class DOMNodeLink extends UI.Widget.Widget {
112
115
  textContent: undefined,
113
116
  isDynamicLink: false,
114
117
  disabled: false,
118
+ ariaDescription: undefined,
115
119
  };
116
120
  const viewInput: ViewInput = {
117
121
  dynamic: options.isDynamicLink,
@@ -129,6 +133,7 @@ export class DOMNodeLink extends UI.Widget.Widget {
129
133
  onMouseLeave: () => {
130
134
  SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
131
135
  },
136
+ ariaDescription: options.ariaDescription,
132
137
  };
133
138
  if (!this.#node) {
134
139
  this.#view(viewInput, {}, this.contentElement);
@@ -270,6 +270,7 @@ interface ComputedStyleWidgetInput {
270
270
  groupComputedStylesSetting: Common.Settings.Setting<boolean>;
271
271
  onFilterChanged: (event: CustomEvent<string>) => void;
272
272
  filterText: string;
273
+ onRegexToggled: () => void;
273
274
  }
274
275
 
275
276
  type View = (input: ComputedStyleWidgetInput, output: null, target: HTMLElement) => void;
@@ -283,8 +284,10 @@ export const DEFAULT_VIEW: View = (input, _output, target) => {
283
284
  <devtools-toolbar-input
284
285
  type="filter"
285
286
  autofocus
287
+ ?regex=${true}
286
288
  value=${input.filterText}
287
289
  @change=${input.onFilterChanged}
290
+ @regextoggle=${input.onRegexToggled}
288
291
  ></devtools-toolbar-input>
289
292
  <devtools-checkbox
290
293
  title=${i18nString(UIStrings.showAll)}
@@ -317,6 +320,7 @@ export class ComputedStyleWidget extends UI.Widget.VBox {
317
320
  #treeData?: TreeOutline.TreeOutline.TreeOutlineData<ComputedStyleData>;
318
321
  readonly #view: View;
319
322
  #filterText = '';
323
+ #isRegex = false;
320
324
 
321
325
  constructor() {
322
326
  super({useShadowDom: true});
@@ -374,6 +378,7 @@ export class ComputedStyleWidget extends UI.Widget.VBox {
374
378
  groupComputedStylesSetting: this.groupComputedStylesSetting,
375
379
  onFilterChanged: this.onFilterChanged.bind(this),
376
380
  filterText: this.#filterText,
381
+ onRegexToggled: this.onRegexToggled.bind(this),
377
382
  },
378
383
  null, this.contentElement);
379
384
  }
@@ -654,10 +659,28 @@ export class ComputedStyleWidget extends UI.Widget.VBox {
654
659
  return result;
655
660
  }
656
661
 
662
+ #buildFilterRegex(text: string): RegExp|null {
663
+ if (!text) {
664
+ return null;
665
+ }
666
+ if (this.#isRegex) {
667
+ try {
668
+ return new RegExp(text, 'i');
669
+ } catch {
670
+ // Invalid regex: fall through to plain-text matching.
671
+ }
672
+ }
673
+ return new RegExp(Platform.StringUtilities.escapeForRegExp(text), 'i');
674
+ }
675
+
676
+ private async onRegexToggled(): Promise<void> {
677
+ this.#isRegex = !this.#isRegex;
678
+ await this.filterComputedStyles(this.#buildFilterRegex(this.#filterText));
679
+ }
680
+
657
681
  private async onFilterChanged(event: CustomEvent<string>): Promise<void> {
658
682
  this.#filterText = event.detail;
659
- await this.filterComputedStyles(
660
- event.detail ? new RegExp(Platform.StringUtilities.escapeForRegExp(event.detail), 'i') : null);
683
+ await this.filterComputedStyles(this.#buildFilterRegex(event.detail));
661
684
 
662
685
  if (event.detail && this.#computedStylesTree.data && this.#computedStylesTree.data.tree) {
663
686
  UI.ARIAUtils.LiveAnnouncer.alert(i18nString(
@@ -73,6 +73,8 @@ const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
73
73
 
74
74
  interface PropertiesWidgetInput {
75
75
  onFilterChanged: (e: CustomEvent<string>) => void;
76
+ onRegexToggled: () => void;
77
+ isRegex: boolean;
76
78
  treeOutlineElement: HTMLElement;
77
79
  displayNoMatchingPropertyMessage: boolean;
78
80
  }
@@ -85,7 +87,13 @@ export const DEFAULT_VIEW: View = (input, _output, target) => {
85
87
  <div jslog=${VisualLogging.pane('element-properties').track({resize: true})}>
86
88
  <div class="hbox properties-widget-toolbar">
87
89
  <devtools-toolbar class="styles-pane-toolbar" role="presentation">
88
- <devtools-toolbar-input type="filter" @change=${input.onFilterChanged} style="flex-grow:1; flex-shrink:1"></devtools-toolbar-input>
90
+ <devtools-toolbar-input
91
+ type="filter"
92
+ ?regex=${true}
93
+ @change=${input.onFilterChanged}
94
+ @regextoggle=${input.onRegexToggled}
95
+ style="flex-grow:1; flex-shrink:1"
96
+ ></devtools-toolbar-input>
89
97
  <devtools-checkbox title=${i18nString(UIStrings.showAllTooltip)} ${bindToSetting(getShowAllPropertiesSetting())}>
90
98
  ${i18nString(UIStrings.showAll)}
91
99
  </devtools-checkbox>
@@ -110,6 +118,8 @@ export class PropertiesWidget extends UI.Widget.VBox {
110
118
  private lastRequestedNode?: SDK.DOMModel.DOMNode;
111
119
  readonly #view: View;
112
120
  #displayNoMatchingPropertyMessage = false;
121
+ #isRegex = false;
122
+ #filterText = '';
113
123
 
114
124
  constructor(view: View = DEFAULT_VIEW) {
115
125
  super({useShadowDom: true});
@@ -140,11 +150,33 @@ export class PropertiesWidget extends UI.Widget.VBox {
140
150
  void this.performUpdate();
141
151
  }
142
152
 
153
+ #buildFilterRegex(text: string): RegExp|null {
154
+ if (!text) {
155
+ return null;
156
+ }
157
+ if (this.#isRegex) {
158
+ try {
159
+ return new RegExp(text, 'i');
160
+ } catch {
161
+ // Invalid regex: fall through to plain-text matching.
162
+ }
163
+ }
164
+ return new RegExp(Platform.StringUtilities.escapeForRegExp(text), 'i');
165
+ }
166
+
143
167
  private onFilterChanged(event: CustomEvent<string>): void {
144
- this.filterRegex = event.detail ? new RegExp(Platform.StringUtilities.escapeForRegExp(event.detail), 'i') : null;
168
+ this.#filterText = event.detail;
169
+ this.filterRegex = this.#buildFilterRegex(event.detail);
145
170
  this.filterAndScheduleUpdate();
146
171
  }
147
172
 
173
+ private onRegexToggled(): void {
174
+ this.#isRegex = !this.#isRegex;
175
+ this.filterRegex = this.#buildFilterRegex(this.#filterText);
176
+ this.internalFilterProperties();
177
+ this.#renderView();
178
+ }
179
+
148
180
  private filterAndScheduleUpdate(): void {
149
181
  const previousDisplay = this.#displayNoMatchingPropertyMessage;
150
182
  this.internalFilterProperties();
@@ -195,9 +227,15 @@ export class PropertiesWidget extends UI.Widget.VBox {
195
227
  treeElement, await root.populateChildrenIfNeeded(), true /* skipProto */, true /* skipGettersAndSetters */);
196
228
  this.internalFilterProperties();
197
229
  }
230
+ this.#renderView();
231
+ }
232
+
233
+ #renderView(): void {
198
234
  this.#view(
199
235
  {
200
236
  onFilterChanged: this.onFilterChanged.bind(this),
237
+ onRegexToggled: this.onRegexToggled.bind(this),
238
+ isRegex: this.#isRegex,
201
239
  treeOutlineElement: this.treeOutline.element,
202
240
  displayNoMatchingPropertyMessage: this.#displayNoMatchingPropertyMessage,
203
241
  },
@@ -184,6 +184,8 @@ export class StylesSidebarPane extends Common.ObjectWrapper.eventMixin<EventType
184
184
  private userOperation = false;
185
185
  isEditingStyle = false;
186
186
  #filterRegex: RegExp|null = null;
187
+ #isRegex = false;
188
+ #filterText = '';
187
189
  private isActivePropertyHighlighted = false;
188
190
  private initialUpdateCompleted = false;
189
191
  hasMatchedStyles = false;
@@ -486,9 +488,28 @@ export class StylesSidebarPane extends Common.ObjectWrapper.eventMixin<EventType
486
488
  }
487
489
  }
488
490
 
491
+ #buildFilterRegex(text: string): RegExp|null {
492
+ if (!text) {
493
+ return null;
494
+ }
495
+ if (this.#isRegex) {
496
+ try {
497
+ return new RegExp(text, 'i');
498
+ } catch {
499
+ // Invalid regex: fall through to plain-text matching.
500
+ }
501
+ }
502
+ return new RegExp(Platform.StringUtilities.escapeForRegExp(text), 'i');
503
+ }
504
+
489
505
  private onFilterChanged(event: Common.EventTarget.EventTargetEvent<string>): void {
490
- const regex = event.data ? new RegExp(Platform.StringUtilities.escapeForRegExp(event.data), 'i') : null;
491
- this.setFilter(regex);
506
+ this.#filterText = event.data;
507
+ this.setFilter(this.#buildFilterRegex(event.data));
508
+ }
509
+
510
+ private onRegexToggled(): void {
511
+ this.#isRegex = !this.#isRegex;
512
+ this.setFilter(this.#buildFilterRegex(this.#filterText));
492
513
  }
493
514
 
494
515
  setFilter(regex: RegExp|null): void {
@@ -1392,7 +1413,9 @@ export class StylesSidebarPane extends Common.ObjectWrapper.eventMixin<EventType
1392
1413
  const hbox = container.createChild('div', 'hbox styles-sidebar-pane-toolbar');
1393
1414
  const toolbar = hbox.createChild('devtools-toolbar', 'styles-pane-toolbar');
1394
1415
  toolbar.role = 'presentation';
1395
- const filterInput = new UI.Toolbar.ToolbarFilter(undefined, 1, 1, undefined, undefined, false);
1416
+ const filterInput = new UI.Toolbar.ToolbarFilter(
1417
+ undefined, 1, 1, undefined, undefined, false, undefined, undefined, /* showRegexToggle=*/ true,
1418
+ this.onRegexToggled.bind(this));
1396
1419
  filterInput.addEventListener(UI.Toolbar.ToolbarInput.Event.TEXT_CHANGED, this.onFilterChanged, this);
1397
1420
  toolbar.appendToolbarItem(filterInput);
1398
1421
  void toolbar.appendItemsAtLocation('styles-sidebarpane-toolbar');
@@ -1621,6 +1621,10 @@ export class NetworkRequestNode extends NetworkNode {
1621
1621
  this.select();
1622
1622
  void action.execute();
1623
1623
  }, {capture: true});
1624
+ // We need this as else the images get open under it.
1625
+ floatingButton.addEventListener('dblclick', ev => {
1626
+ ev.stopPropagation();
1627
+ }, {capture: true});
1624
1628
  floatingButton.addEventListener('mousedown', ev => {
1625
1629
  ev.stopPropagation();
1626
1630
  }, {capture: true});