chrome-devtools-frontend 1.0.1593959 → 1.0.1595925

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 (79) hide show
  1. package/docs/contributing/settings-experiments-features.md +25 -5
  2. package/front_end/core/common/README.md +126 -0
  3. package/front_end/core/sdk/CSSModel.ts +22 -0
  4. package/front_end/core/sdk/CSSNavigation.ts +33 -0
  5. package/front_end/core/sdk/CSSRule.ts +12 -2
  6. package/front_end/core/sdk/sdk-meta.ts +22 -18
  7. package/front_end/core/sdk/sdk.ts +2 -0
  8. package/front_end/design_system_tokens.css +538 -259
  9. package/front_end/entrypoints/main/main-meta.ts +2 -2
  10. package/front_end/generated/InspectorBackendCommands.ts +3 -5
  11. package/front_end/generated/SupportedCSSProperties.js +12 -0
  12. package/front_end/generated/protocol-mapping.d.ts +0 -8
  13. package/front_end/generated/protocol-proxy-api.d.ts +0 -6
  14. package/front_end/generated/protocol.ts +1 -19
  15. package/front_end/models/issues_manager/Issue.ts +1 -0
  16. package/front_end/models/issues_manager/IssueAggregator.ts +9 -0
  17. package/front_end/models/issues_manager/IssuesManager.ts +5 -0
  18. package/front_end/models/issues_manager/SelectivePermissionsInterventionIssue.ts +65 -0
  19. package/front_end/models/issues_manager/descriptions/selectivePermissionsIntervention.md +7 -0
  20. package/front_end/models/issues_manager/issues_manager.ts +2 -0
  21. package/front_end/models/javascript_metadata/NativeFunctions.js +49 -10
  22. package/front_end/models/persistence/persistence-meta.ts +4 -4
  23. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +12 -1
  24. package/front_end/panels/ai_assistance/components/ChatInput.ts +37 -30
  25. package/front_end/panels/ai_assistance/components/ChatView.ts +4 -2
  26. package/front_end/panels/ai_assistance/components/WalkthroughView.ts +10 -7
  27. package/front_end/panels/ai_assistance/components/chatInput.css +26 -1
  28. package/front_end/panels/ai_assistance/components/walkthroughView.css +15 -0
  29. package/front_end/panels/application/StorageView.ts +8 -2
  30. package/front_end/panels/application/preloading/PreloadingView.ts +105 -7
  31. package/front_end/panels/application/preloading/preloadingView.css +4 -0
  32. package/front_end/panels/console/ConsoleView.ts +2 -2
  33. package/front_end/panels/console/console-meta.ts +18 -14
  34. package/front_end/panels/elements/ComputedStyleWidget.ts +12 -7
  35. package/front_end/panels/elements/ElementsPanel.ts +1 -1
  36. package/front_end/panels/elements/ElementsTreeOutline.ts +4 -6
  37. package/front_end/panels/elements/ImagePreviewPopover.ts +6 -9
  38. package/front_end/panels/elements/StylePropertiesSection.ts +30 -0
  39. package/front_end/panels/elements/StylesSidebarPane.ts +13 -7
  40. package/front_end/panels/elements/elements-meta.ts +12 -8
  41. package/front_end/panels/issues/AffectedSelectivePermissionsInterventionView.ts +120 -0
  42. package/front_end/panels/issues/IssueView.ts +2 -0
  43. package/front_end/panels/issues/IssuesPane.ts +6 -0
  44. package/front_end/panels/issues/issues.ts +2 -0
  45. package/front_end/panels/lighthouse/LighthouseStartView.ts +99 -38
  46. package/front_end/panels/lighthouse/LighthouseTimespanView.ts +53 -14
  47. package/front_end/panels/lighthouse/RadioSetting.ts +33 -19
  48. package/front_end/panels/lighthouse/lighthouse.ts +2 -0
  49. package/front_end/panels/media/PlayerMessagesView.ts +62 -49
  50. package/front_end/panels/media/media.ts +2 -0
  51. package/front_end/panels/media/playerMessagesView.css +1 -1
  52. package/front_end/panels/profiler/HeapSnapshotGridNodes.ts +49 -24
  53. package/front_end/panels/settings/FrameworkIgnoreListSettingsTab.ts +2 -2
  54. package/front_end/panels/sources/sources-meta.ts +8 -4
  55. package/front_end/panels/utils/utils.ts +11 -5
  56. package/front_end/third_party/chromium/README.chromium +1 -1
  57. package/front_end/third_party/lit/lib/async-directive.d.ts +465 -0
  58. package/front_end/third_party/lit/lib/async-directive.js +23 -0
  59. package/front_end/third_party/lit/lib/async-directive.js.map +1 -0
  60. package/front_end/third_party/lit/lib/decorators.d.ts +7 -1
  61. package/front_end/third_party/lit/lib/decorators.js +2 -2
  62. package/front_end/third_party/lit/lib/decorators.js.map +1 -1
  63. package/front_end/third_party/lit/lib/directive.js.map +1 -1
  64. package/front_end/third_party/lit/lib/directives.d.ts +27 -8
  65. package/front_end/third_party/lit/lib/directives.js +8 -8
  66. package/front_end/third_party/lit/lib/directives.js.map +1 -1
  67. package/front_end/third_party/lit/lib/lit.d.ts +15 -5
  68. package/front_end/third_party/lit/lib/lit.js +4 -4
  69. package/front_end/third_party/lit/lib/lit.js.map +1 -1
  70. package/front_end/third_party/lit/lib/static-html.js +2 -2
  71. package/front_end/third_party/lit/lib/static-html.js.map +1 -1
  72. package/front_end/third_party/lit/lit.ts +2 -1
  73. package/front_end/third_party/lit/rollup.config.mjs +1 -1
  74. package/front_end/ui/components/markdown_view/MarkdownLinksMap.ts +5 -0
  75. package/front_end/ui/legacy/components/utils/ImagePreview.ts +27 -23
  76. package/front_end/ui/lit/lit.ts +1 -0
  77. package/front_end/ui/visual_logging/Debugging.ts +1 -1
  78. package/front_end/ui/visual_logging/KnownContextValues.ts +4 -0
  79. package/package.json +1 -1
@@ -693,7 +693,15 @@ export class AiAssistancePanel extends UI.Panel.Panel {
693
693
  blockedByCrossOrigin: this.#conversation.isBlockedByOrigin,
694
694
  isLoading: this.#isLoading,
695
695
  messages: this.#messages,
696
- selectedContext: this.#conversation.selectedContext ?? null,
696
+ /**
697
+ * We pass either the selected context with isContextSelected=true
698
+ * to make sure the pill is show with normal styling and a remove button.
699
+ * Or we pass the panels default context with isContextSelected=false
700
+ * to display a placeholder pill with neutral styling and an add button.
701
+ */
702
+ context:
703
+ this.#conversation.selectedContext ?? this.#getConversationContext(this.#getDefaultConversationType()),
704
+ isContextSelected: Boolean(this.#conversation.selectedContext),
697
705
  conversationType: this.#conversation.type,
698
706
  isReadOnly: this.#conversation.isReadOnly ?? false,
699
707
  changeSummary: this.#getChangeSummary(),
@@ -857,17 +865,20 @@ export class AiAssistancePanel extends UI.Panel.Panel {
857
865
  #selectDefaultAgentIfNeeded(): void {
858
866
  // We don't change the current agent when there is a message in flight.
859
867
  if (this.#isLoading) {
868
+ this.requestUpdate();
860
869
  return;
861
870
  }
862
871
 
863
872
  // If there already is an agent and if it is not empty,
864
873
  // we don't automatically change the agent.
865
874
  if (this.#conversation && !this.#conversation.isEmpty) {
875
+ this.requestUpdate();
866
876
  return;
867
877
  }
868
878
 
869
879
  const targetConversationType = this.#getDefaultConversationType();
870
880
  if (this.#conversation?.type === targetConversationType) {
881
+ this.requestUpdate();
871
882
  // The above if makes sure even if we have an active agent it's empty
872
883
  // So we can just reuse it
873
884
  return;
@@ -134,7 +134,8 @@ export interface ViewInput {
134
134
  blockedByCrossOrigin: boolean;
135
135
  isTextInputDisabled: boolean;
136
136
  inputPlaceholder: Platform.UIString.LocalizedString;
137
- selectedContext: AiAssistanceModel.AiAgent.ConversationContext<unknown>|null;
137
+ context: AiAssistanceModel.AiAgent.ConversationContext<unknown>|null;
138
+ isContextSelected: boolean;
138
139
  inspectElementToggled: boolean;
139
140
  disclaimerText: string;
140
141
  conversationType: AiAssistanceModel.AiHistoryStorage.ConversationType;
@@ -183,7 +184,7 @@ function getContextRemoveLabel(context: AiAssistanceModel.AiAgent.ConversationCo
183
184
  export const DEFAULT_VIEW = (input: ViewInput, _output: ViewOutput, target: HTMLElement): void => {
184
185
  const chatInputContainerCls = Lit.Directives.classMap({
185
186
  'chat-input-container': true,
186
- 'single-line-layout': !input.selectedContext && !input.onContextAdd,
187
+ 'single-line-layout': !input.context,
187
188
  disabled: input.isTextInputDisabled,
188
189
  });
189
190
 
@@ -300,7 +301,7 @@ export const DEFAULT_VIEW = (input: ViewInput, _output: ViewOutput, target: HTML
300
301
  ></textarea>
301
302
  <div class="chat-input-actions">
302
303
  <div class="chat-input-actions-left">
303
- ${input.selectedContext ?
304
+ ${input.context ?
304
305
  html`
305
306
  <div class="select-element">
306
307
  ${input.conversationType === AiAssistanceModel.AiHistoryStorage.ConversationType.STYLING ?
@@ -321,29 +322,33 @@ export const DEFAULT_VIEW = (input: ViewInput, _output: ViewOutput, target: HTML
321
322
  ></devtools-button>`
322
323
  : Lit.nothing}
323
324
  <div
324
- class="resource-link"
325
+ class=${Lit.Directives.classMap({
326
+ 'resource-link': true,
327
+ disabled: !input.isContextSelected,
328
+ })}
325
329
  >
326
330
  ${
327
- input.selectedContext instanceof AiAssistanceModel.StylingAgent.NodeContext ?
331
+ input.context instanceof AiAssistanceModel.StylingAgent.NodeContext ?
328
332
  html`
329
333
  <devtools-widget
330
334
  class="title"
331
335
  .widgetConfig=${UI.Widget.widgetConfig(PanelsCommon.DOMLinkifier.DOMNodeLink, {
332
- node: input.selectedContext.getItem(),
336
+ node: input.context.getItem(),
333
337
  options: {
334
- hiddenClassList: input.selectedContext.getItem().classNames().filter(
338
+ disabled: !input.isContextSelected,
339
+ hiddenClassList: input.context.getItem().classNames().filter(
335
340
  className => className.startsWith(AiAssistanceModel.Injected.AI_ASSISTANCE_CSS_CLASS_NAME)),
336
341
  ariaDescription: i18nString(UIStrings.revealContextDescription),
337
342
  },
338
343
  })}
339
344
  ></devtools-widget>` :
340
345
  html`
341
- ${input.selectedContext instanceof AiAssistanceModel.NetworkAgent.RequestContext ?
342
- PanelUtils.PanelUtils.getIconForNetworkRequest(input.selectedContext.getItem()) :
343
- input.selectedContext instanceof AiAssistanceModel.FileAgent.FileContext ?
344
- PanelUtils.PanelUtils.getIconForSourceFile(input.selectedContext.getItem()) :
345
- input.selectedContext instanceof AiAssistanceModel.PerformanceAgent.PerformanceTraceContext ?
346
- html`<devtools-icon name="performance" title="Performance"></devtools-icon>` :
346
+ ${input.context instanceof AiAssistanceModel.NetworkAgent.RequestContext ?
347
+ PanelUtils.PanelUtils.getIconForNetworkRequest(input.context.getItem()) :
348
+ input.context instanceof AiAssistanceModel.FileAgent.FileContext ?
349
+ PanelUtils.PanelUtils.getIconForSourceFile(input.context.getItem()) :
350
+ input.context instanceof AiAssistanceModel.PerformanceAgent.PerformanceTraceContext ?
351
+ html`<devtools-icon class="icon" name="performance" title="Performance"></devtools-icon>` :
347
352
  Lit.nothing}
348
353
  <span
349
354
  role="button"
@@ -356,31 +361,31 @@ export const DEFAULT_VIEW = (input: ViewInput, _output: ViewOutput, target: HTML
356
361
  }
357
362
  }}
358
363
  aria-description=${i18nString(UIStrings.revealContextDescription)}
359
- >${input.selectedContext.getTitle()}</span>`
364
+ >${input.context.getTitle()}</span>`
360
365
  }
361
- ${input.onContextRemoved ? html`
366
+ ${input.isContextSelected && input.onContextRemoved ? html`
362
367
  <devtools-button
363
- title=${getContextRemoveLabel(input.selectedContext)}
364
- aria-label=${getContextRemoveLabel(input.selectedContext)}
368
+ title=${getContextRemoveLabel(input.context)}
369
+ aria-label=${getContextRemoveLabel(input.context)}
365
370
  class="remove-context"
366
371
  .iconName=${'cross'}
367
372
  .size=${Buttons.Button.Size.MICRO}
368
373
  .jslogContext=${'context-removed'}
369
374
  .variant=${Buttons.Button.Variant.ICON}
370
375
  @click=${input.onContextRemoved}></devtools-button>` : Lit.nothing}
376
+ ${!input.isContextSelected && input.onContextAdd ? html`
377
+ <devtools-button
378
+ title=${lockedString(UIStringsNotTranslate.addContext)}
379
+ aria-label=${lockedString(UIStringsNotTranslate.addContext)}
380
+ class="add-context"
381
+ .iconName=${'plus'}
382
+ .size=${Buttons.Button.Size.MICRO}
383
+ .jslogContext=${'context-added'}
384
+ .variant=${Buttons.Button.Variant.ICON}
385
+ @click=${input.onContextAdd}></devtools-button>` : Lit.nothing}
371
386
  </div>
372
387
  </div>`
373
- :
374
- input.onContextAdd ? html`
375
- <devtools-button
376
- title=${lockedString(UIStringsNotTranslate.addContext)}
377
- aria-label=${lockedString(UIStringsNotTranslate.addContext)}
378
- class="add-context"
379
- .iconName=${'plus'}
380
- .size=${Buttons.Button.Size.SMALL}
381
- .jslogContext=${'context-added'}
382
- .variant=${Buttons.Button.Variant.ICON}
383
- @click=${input.onContextAdd}></devtools-button>` : Lit.nothing}
388
+ : Lit.nothing}
384
389
  </div>
385
390
  <div class="chat-input-actions-right">
386
391
  <div class="chat-input-disclaimer-container">
@@ -488,7 +493,8 @@ export class ChatInput extends UI.Widget.Widget implements SDK.TargetManager.Obs
488
493
  blockedByCrossOrigin = false;
489
494
  isTextInputDisabled = false;
490
495
  inputPlaceholder = '' as Platform.UIString.LocalizedString;
491
- selectedContext: AiAssistanceModel.AiAgent.ConversationContext<unknown>|null = null;
496
+ context: AiAssistanceModel.AiAgent.ConversationContext<unknown>|null = null;
497
+ isContextSelected = false;
492
498
  inspectElementToggled = false;
493
499
  disclaimerText = '';
494
500
  conversationType = AiAssistanceModel.AiHistoryStorage.ConversationType.STYLING;
@@ -675,7 +681,8 @@ export class ChatInput extends UI.Widget.Widget implements SDK.TargetManager.Obs
675
681
  isLoading: this.isLoading,
676
682
  blockedByCrossOrigin: this.blockedByCrossOrigin,
677
683
  isTextInputDisabled: this.isTextInputDisabled,
678
- selectedContext: this.selectedContext,
684
+ context: this.context,
685
+ isContextSelected: this.isContextSelected,
679
686
  inspectElementToggled: this.inspectElementToggled,
680
687
  isTextInputEmpty: this.#isTextInputEmpty(),
681
688
  disclaimerText: this.disclaimerText,
@@ -65,7 +65,8 @@ export interface Props {
65
65
  changeManager: AiAssistanceModel.ChangeManager.ChangeManager;
66
66
  inspectElementToggled: boolean;
67
67
  messages: Message[];
68
- selectedContext: AiAssistanceModel.AiAgent.ConversationContext<unknown>|null;
68
+ context: AiAssistanceModel.AiAgent.ConversationContext<unknown>|null;
69
+ isContextSelected: boolean;
69
70
  isLoading: boolean;
70
71
  canShowFeedbackForm: boolean;
71
72
  userInfo: Pick<Host.InspectorFrontendHostAPI.SyncInformation, 'accountImage'|'accountGivenName'>;
@@ -178,7 +179,8 @@ const DEFAULT_VIEW: View = (input, output, target) => {
178
179
  isTextInputDisabled: input.isTextInputDisabled,
179
180
  inputPlaceholder: input.inputPlaceholder,
180
181
  disclaimerText: input.disclaimerText,
181
- selectedContext: input.selectedContext,
182
+ context: input.context,
183
+ isContextSelected: input.isContextSelected,
182
184
  inspectElementToggled: input.inspectElementToggled,
183
185
  multimodalInputEnabled: input.multimodalInputEnabled ?? false,
184
186
  conversationType: input.conversationType,
@@ -117,13 +117,16 @@ export const DEFAULT_VIEW = (
117
117
  const stepsOutput = steps.length > 0 ? html`
118
118
  <div class="steps-container">
119
119
  ${steps.map((step, index) => html`
120
- <div class="step-wrapper">
121
- ${renderStep({
122
- step,
123
- isLoading: input.isLoading,
124
- markdownRenderer: input.markdownRenderer,
125
- isLast: index === steps.length - 1
126
- })}
120
+ <div class="walkthrough-step">
121
+ <span class="step-number">${index+1}</span>
122
+ <div class="step-wrapper">
123
+ ${renderStep({
124
+ step,
125
+ isLoading: input.isLoading,
126
+ markdownRenderer: input.markdownRenderer,
127
+ isLast: index === steps.length - 1
128
+ })}
129
+ </div>
127
130
  </div>
128
131
  `)}
129
132
  </div>
@@ -239,12 +239,15 @@
239
239
 
240
240
  & .title {
241
241
  vertical-align: middle;
242
+ /* Fixed italic text getting cut off */
243
+ padding-right: var(--sys-size-2);
242
244
  font: var(--sys-typescale-body5-regular);
243
245
  overflow: hidden;
244
246
  text-overflow: ellipsis;
245
247
  }
246
248
 
247
- & .remove-context {
249
+ & .remove-context,
250
+ & .add-context {
248
251
  vertical-align: middle;
249
252
  }
250
253
 
@@ -260,6 +263,28 @@
260
263
  min-height: var(--sys-size-7);
261
264
  }
262
265
 
266
+ &.disabled {
267
+ border-style: dashed;
268
+ border-color: var(--sys-color-neutral-outline);
269
+ color: var(--sys-color-on-surface-light);
270
+
271
+ devtools-icon,
272
+ devtools-file-source-icon {
273
+ /* Override devtools-file-source-icon */
274
+ --override-file-source-icon-color: var(
275
+ --sys-color-on-surface-light-graphics
276
+ );
277
+ /* Some icons set their style attribute and we need to override it */
278
+ /* stylelint-disable-next-line declaration-no-important */
279
+ color: var(--sys-color-on-surface-light-graphics) !important;
280
+ }
281
+
282
+ .title {
283
+ color: var(--sys-color-on-surface-light);
284
+ font-style: italic;
285
+ }
286
+ }
287
+
263
288
  /*
264
289
  CSS styling for `network-override-marker` is similar to
265
290
  https://source.chromium.org/chromium/chromium/src/+/main:third_party/devtools-frontend/src/front_end/panels/network/networkLogView.css;l=379.
@@ -38,6 +38,21 @@
38
38
  gap: var(--sys-size-6);
39
39
  }
40
40
 
41
+ .walkthrough-step {
42
+ display: flex;
43
+ gap: var(--sys-size-6);
44
+ align-items: flex-start;
45
+ justify-content: flex-start;
46
+
47
+ .step-number {
48
+ font: var(--sys-typescale-body4-regular);
49
+ color: var(--sys-color-on-surface-subtle);
50
+ padding-top:var(--sys-size-4);
51
+ flex-grow: 0;
52
+ flex-shrink: 0;
53
+ }
54
+ }
55
+
41
56
  .step-wrapper {
42
57
  display: flex;
43
58
  flex-direction: column;
@@ -510,8 +510,14 @@ export class StorageView extends UI.Widget.VBox {
510
510
  const quotaAsString = i18n.ByteUtilities.bytesToString(response.quota);
511
511
  const usageAsString = i18n.ByteUtilities.bytesToString(response.usage);
512
512
  const formattedQuotaAsString = i18nString(UIStrings.storageWithCustomMarker, {PH1: quotaAsString});
513
- const quota =
514
- quotaOverridden ? UI.Fragment.Fragment.build`<b>${formattedQuotaAsString}</b>`.element() : quotaAsString;
513
+
514
+ let quota: string|HTMLElement = quotaAsString;
515
+ if (quotaOverridden) {
516
+ const element = document.createElement('b');
517
+ element.textContent = formattedQuotaAsString;
518
+ quota = element;
519
+ }
520
+
515
521
  const element = uiI18n.getFormatLocalizedString(str_, UIStrings.storageQuotaUsed, {PH1: usageAsString, PH2: quota});
516
522
  this.quotaRow.appendChild(element);
517
523
  UI.Tooltip.Tooltip.install(
@@ -13,6 +13,7 @@ import * as Platform from '../../../core/platform/platform.js';
13
13
  import {assertNotNullOrUndefined} from '../../../core/platform/platform.js';
14
14
  import * as SDK from '../../../core/sdk/sdk.js';
15
15
  import * as Protocol from '../../../generated/protocol.js';
16
+ import * as TextUtils from '../../../models/text_utils/text_utils.js';
16
17
  import * as Buttons from '../../../ui/components/buttons/buttons.js';
17
18
  // eslint-disable-next-line @devtools/es-modules-import
18
19
  import emptyWidgetStyles from '../../../ui/legacy/emptyWidget.css.js';
@@ -21,7 +22,7 @@ import {Directives, html, render} from '../../../ui/lit/lit.js';
21
22
  import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';
22
23
 
23
24
  import * as PreloadingComponents from './components/components.js';
24
- import {ruleSetTagOrLocationShort} from './components/PreloadingString.js';
25
+ import {capitalizedAction, ruleSetTagOrLocationShort} from './components/PreloadingString.js';
25
26
  import * as PreloadingHelper from './helper/helper.js';
26
27
  import preloadingViewStyles from './preloadingView.css.js';
27
28
  import preloadingViewDropDownStyles from './preloadingViewDropDown.css.js';
@@ -362,6 +363,78 @@ export class PreloadingRuleSetView extends UI.Widget.VBox {
362
363
  }
363
364
  }
364
365
 
366
+ /**
367
+ * Pure filtering function for preloading grid rows.
368
+ * Exported for testability.
369
+ */
370
+ export function applyFilterText(filterText: string, rows: PreloadingComponents.PreloadingGrid.PreloadingGridRow[]):
371
+ PreloadingComponents.PreloadingGrid.PreloadingGridRow[] {
372
+ const trimmedFilter = filterText.trim();
373
+ if (trimmedFilter === '') {
374
+ return rows;
375
+ }
376
+
377
+ const FILTER_KEYS = ['url', 'action', 'status'] as const;
378
+ const parser = new TextUtils.TextUtils.FilterParser([...FILTER_KEYS]);
379
+ // The match is case-insensitive. We handle the matching with everything lower cased,
380
+ // both keywords and values.
381
+ const query = parser.parse(filterText.toLowerCase());
382
+
383
+ // Drop the last term if it is an incomplete filter key (e.g., "action:" with no value).
384
+ // FilterParser parses "action:" as plain text since KEY_VALUE_FILTER_REGEXP requires a value.
385
+ // This lets users type "url:foo action:" and still see results for "url:foo".
386
+ const lastTerm = query.at(-1);
387
+
388
+ // If the parse result is empty, the query only contains spaces.
389
+ if (!lastTerm) {
390
+ return rows;
391
+ }
392
+
393
+ const isKeyWithNoValue =
394
+ (lastTerm.key === undefined || lastTerm.key === null) && FILTER_KEYS.some(key => lastTerm.text === `${key}:`);
395
+ if (isKeyWithNoValue) {
396
+ query.pop();
397
+ }
398
+
399
+ if (query.length === 0) {
400
+ return rows;
401
+ }
402
+
403
+ return rows.filter(row => {
404
+ const attempt = row.pipeline.getOriginallyTriggered();
405
+ const url = attempt.key.url.toLowerCase();
406
+ const action = capitalizedAction(attempt.action).toLowerCase();
407
+ const status = PreloadingUIUtils.status(attempt.status).toLowerCase();
408
+
409
+ // Each term must match (AND logic between terms)
410
+ return query.every(term => {
411
+ if (term.text === undefined || term.text === null || term.text === '') {
412
+ return true;
413
+ }
414
+ const searchText = term.text.toLowerCase();
415
+
416
+ // The query is lowercased before parsing, so keys are already normalized.
417
+ const key = term.key;
418
+ switch (key) {
419
+ case 'url':
420
+ return url.includes(searchText);
421
+ case 'action':
422
+ return action.includes(searchText);
423
+ case 'status': {
424
+ // Support multiple status values separated by comma (e.g., "status:ready,success")
425
+ const statusValues = searchText.split(',');
426
+ return statusValues.some(v => status.includes(v));
427
+ }
428
+ case undefined:
429
+ // No key specified: search across all columns (URL, action, status)
430
+ return url.includes(searchText) || action.includes(searchText) || status.includes(searchText);
431
+ default:
432
+ return false;
433
+ }
434
+ });
435
+ });
436
+ }
437
+
365
438
  export class PreloadingAttemptView extends UI.Widget.VBox {
366
439
  private model: SDK.PreloadingModel.PreloadingModel;
367
440
  // Note that we use id of (representative) preloading attempt while we show pipelines in grid.
@@ -374,6 +447,7 @@ export class PreloadingAttemptView extends UI.Widget.VBox {
374
447
  private readonly preloadingDetails =
375
448
  new PreloadingComponents.PreloadingDetailsReportView.PreloadingDetailsReportView();
376
449
  private readonly ruleSetSelector: PreloadingRuleSetSelector;
450
+ private readonly textFilterUI: UI.Toolbar.ToolbarFilter;
377
451
  private clearButton: UI.Toolbar.ToolbarButton;
378
452
 
379
453
  constructor(model: SDK.PreloadingModel.PreloadingModel) {
@@ -417,7 +491,19 @@ export class PreloadingAttemptView extends UI.Widget.VBox {
417
491
  const toolbar = vbox.contentElement.createChild('devtools-toolbar', 'preloading-toolbar');
418
492
  toolbar.setAttribute('jslog', `${VisualLogging.toolbar()}`);
419
493
 
420
- // Clear button first (leftmost)
494
+ // Rule set dropdown first
495
+ this.ruleSetSelector = new PreloadingRuleSetSelector(() => this.render());
496
+ toolbar.appendToolbarItem(this.ruleSetSelector.item());
497
+
498
+ // Text filter second
499
+ this.textFilterUI = new UI.Toolbar.ToolbarFilter(undefined, 1, 1);
500
+ this.textFilterUI.addEventListener(UI.Toolbar.ToolbarInput.Event.TEXT_CHANGED, this.onTextFilterChanged, this);
501
+ toolbar.appendToolbarItem(this.textFilterUI);
502
+
503
+ // Separator between text filter and clear button
504
+ toolbar.appendToolbarItem(new UI.Toolbar.ToolbarSeparator());
505
+
506
+ // Clear button last (rightmost)
421
507
  this.clearButton =
422
508
  new UI.Toolbar.ToolbarButton('Clear speculative loads', 'clear', undefined, 'clear-speculative-loads');
423
509
  this.clearButton.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, () => {
@@ -428,14 +514,13 @@ export class PreloadingAttemptView extends UI.Widget.VBox {
428
514
  }
429
515
 
430
516
  model.reset();
517
+ // Reset UI state
518
+ this.textFilterUI.setValue('');
431
519
  this.ruleSetSelector.select(null);
520
+ this.render();
432
521
  });
433
522
  toolbar.appendToolbarItem(this.clearButton);
434
523
 
435
- // Rule set dropdown
436
- this.ruleSetSelector = new PreloadingRuleSetSelector(() => this.render());
437
- toolbar.appendToolbarItem(this.ruleSetSelector.item());
438
-
439
524
  this.preloadingGrid.onSelect = this.onPreloadingGridCellFocused.bind(this);
440
525
 
441
526
  const preloadingGridContainer = document.createElement('div');
@@ -491,6 +576,14 @@ export class PreloadingAttemptView extends UI.Widget.VBox {
491
576
  }
492
577
 
493
578
  this.ruleSetSelector.select(id);
579
+
580
+ // Reset text filter when navigating from Rules view to clear any existing filter
581
+ this.textFilterUI.setValue('');
582
+ this.render();
583
+ }
584
+
585
+ private onTextFilterChanged(): void {
586
+ this.render();
494
587
  }
495
588
 
496
589
  private updatePreloadingDetails(): void {
@@ -532,8 +625,13 @@ export class PreloadingAttemptView extends UI.Widget.VBox {
532
625
  statusCode,
533
626
  };
534
627
  });
535
- this.preloadingGrid.rows = rows;
628
+
629
+ // Apply text filter
630
+ const filteredRows = applyFilterText(this.textFilterUI.valueWithoutSuggestion(), rows);
631
+
632
+ this.preloadingGrid.rows = filteredRows;
536
633
  this.preloadingGrid.pageURL = pageURL();
634
+ // Only show empty state when there are truly no speculations (not when filter has no matches)
537
635
  this.contentElement.classList.toggle('empty', rows.length === 0);
538
636
 
539
637
  this.updatePreloadingDetails();
@@ -25,6 +25,10 @@
25
25
  button.toolbar-has-dropdown {
26
26
  margin: var(--sys-size-2) 0;
27
27
  }
28
+
29
+ .toolbar-filter {
30
+ max-width: var(--sys-size-29);
31
+ }
28
32
  }
29
33
 
30
34
  devtools-split-view {
@@ -107,11 +107,11 @@ const UIStrings = {
107
107
  /**
108
108
  * @description Title of a setting under the Console category that can be invoked through the Command Menu
109
109
  */
110
- groupSimilarMessagesInConsole: 'Group similar messages in console',
110
+ groupSimilarMessagesInConsole: 'Group similar messages',
111
111
  /**
112
112
  * @description Title of a setting under the Console category that can be invoked through the Command Menu
113
113
  */
114
- showCorsErrorsInConsole: 'Show `CORS` errors in console',
114
+ showCorsErrorsInConsole: 'CORS errors in console',
115
115
  /**
116
116
  * @description Tooltip for the the console sidebar toggle in the Console panel. Command to
117
117
  * open/show the sidebar.
@@ -62,11 +62,11 @@ const UIStrings = {
62
62
  */
63
63
  timestamps: 'Timestamps',
64
64
  /**
65
- * @description Title of a setting under the Console category that can be invoked through the Command Menu
65
+ * @description Title of an option under the Console category that can be invoked through the Command Menu
66
66
  */
67
67
  showTimestamps: 'Show timestamps',
68
68
  /**
69
- * @description Title of a setting under the Console category that can be invoked through the Command Menu
69
+ * @description Title of an option under the Console category that can be invoked through the Command Menu
70
70
  */
71
71
  hideTimestamps: 'Hide timestamps',
72
72
  /**
@@ -88,19 +88,23 @@ const UIStrings = {
88
88
  /**
89
89
  * @description Title of a setting under the Console category that can be invoked through the Command Menu
90
90
  */
91
- groupSimilarMessagesInConsole: 'Group similar messages in console',
91
+ groupSimilarMessages: 'Group similar messages',
92
92
  /**
93
93
  * @description Title of a setting under the Console category that can be invoked through the Command Menu
94
94
  */
95
- doNotGroupSimilarMessagesIn: 'Do not group similar messages in console',
95
+ doNotGroupSimilarMessages: 'Don\'t group similar messages',
96
96
  /**
97
- * @description Title of a setting under the Console category that can be invoked through the Command Menu
97
+ * @description Title of a setting under the Console category in Settings
98
98
  */
99
- showCorsErrorsInConsole: 'Show `CORS` errors in console',
99
+ corsErrorsInConsole: 'CORS errors in console',
100
100
  /**
101
- * @description Title of a setting under the Console category that can be invoked through the Command Menu
101
+ * @description Title of an option under the Console category that can be invoked through the Command Menu
102
+ */
103
+ showCorsErrorsInConsole: 'Show CORS errors in console',
104
+ /**
105
+ * @description Title of an option under the Console category that can be invoked through the Command Menu
102
106
  */
103
- doNotShowCorsErrorsIn: 'Do not show `CORS` errors in console',
107
+ doNotShowCorsErrorsIn: 'Don\'t show CORS errors in console',
104
108
  /**
105
109
  * @description Title of a setting under the Console category in Settings
106
110
  */
@@ -137,7 +141,7 @@ const UIStrings = {
137
141
  * @description Title of a setting under the Console category in Settings that controls whether AI summaries should
138
142
  * be shown for console warnings/errors.
139
143
  */
140
- showConsoleInsightTeasers: 'Show AI summaries for console messages',
144
+ consoleInsightTeasers: 'AI summaries for console messages',
141
145
  } as const;
142
146
  const str_ = i18n.i18n.registerUIStrings('panels/console/console-meta.ts', UIStrings);
143
147
  const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_);
@@ -342,25 +346,25 @@ Common.Settings.registerSettingExtension({
342
346
  Common.Settings.registerSettingExtension({
343
347
  category: Common.Settings.SettingCategory.CONSOLE,
344
348
  storageType: Common.Settings.SettingStorageType.SYNCED,
345
- title: i18nLazyString(UIStrings.groupSimilarMessagesInConsole),
349
+ title: i18nLazyString(UIStrings.groupSimilarMessages),
346
350
  settingName: 'console-group-similar',
347
351
  settingType: Common.Settings.SettingType.BOOLEAN,
348
352
  defaultValue: true,
349
353
  options: [
350
354
  {
351
355
  value: true,
352
- title: i18nLazyString(UIStrings.groupSimilarMessagesInConsole),
356
+ title: i18nLazyString(UIStrings.groupSimilarMessages),
353
357
  },
354
358
  {
355
359
  value: false,
356
- title: i18nLazyString(UIStrings.doNotGroupSimilarMessagesIn),
360
+ title: i18nLazyString(UIStrings.doNotGroupSimilarMessages),
357
361
  },
358
362
  ],
359
363
  });
360
364
 
361
365
  Common.Settings.registerSettingExtension({
362
366
  category: Common.Settings.SettingCategory.CONSOLE,
363
- title: i18nLazyString(UIStrings.showCorsErrorsInConsole),
367
+ title: i18nLazyString(UIStrings.corsErrorsInConsole),
364
368
  settingName: 'console-shows-cors-errors',
365
369
  settingType: Common.Settings.SettingType.BOOLEAN,
366
370
  defaultValue: true,
@@ -436,7 +440,7 @@ Common.Settings.registerSettingExtension({
436
440
  Common.Settings.registerSettingExtension({
437
441
  category: Common.Settings.SettingCategory.CONSOLE,
438
442
  storageType: Common.Settings.SettingStorageType.SYNCED,
439
- title: i18nLazyString(UIStrings.showConsoleInsightTeasers),
443
+ title: i18nLazyString(UIStrings.consoleInsightTeasers),
440
444
  settingName: 'console-insight-teasers-enabled',
441
445
  settingType: Common.Settings.SettingType.BOOLEAN,
442
446
  defaultValue: true,
@@ -353,13 +353,18 @@ export class ComputedStyleWidget extends UI.Widget.VBox {
353
353
 
354
354
  this.filterRegex = null;
355
355
  this.linkifier = new Components.Linkifier.Linkifier(maxLinkLength);
356
- this.imagePreviewPopover = new ImagePreviewPopover(this.contentElement, event => {
357
- const link = event.composedPath()[0];
358
- if (link instanceof Element) {
359
- return link;
360
- }
361
- return null;
362
- }, () => this.#computedStyleModel ? this.#computedStyleModel.node : null);
356
+ this.imagePreviewPopover = new ImagePreviewPopover(
357
+ this.contentElement,
358
+ event => {
359
+ const link = event.composedPath()[0];
360
+ if (link instanceof Element) {
361
+ return link;
362
+ }
363
+ return null;
364
+ },
365
+ async () => {
366
+ return await Components.ImagePreview.loadPrecomputedFeatures(this.#computedStyleModel?.node);
367
+ });
363
368
 
364
369
  this.#updateView({hasMatches: true});
365
370
  }
@@ -1556,7 +1556,7 @@ export class DOMNodeRevealer implements
1556
1556
  }
1557
1557
 
1558
1558
  if (resolvedNode) {
1559
- const opts: RevealAndSelectNodeOpts = {showPanel: true, focusNode: !omitFocus};
1559
+ const opts: RevealAndSelectNodeOpts = omitFocus ? {showPanel: false} : {showPanel: true, focusNode: true};
1560
1560
  const promise = resolvedNode instanceof SDK.DOMModel.AdoptedStyleSheet ?
1561
1561
  panel.revealAndSelectAdoptedStyleSheet(resolvedNode, opts) :
1562
1562
  panel.revealAndSelectNode(resolvedNode, opts);