chrome-devtools-frontend 1.0.1643855 → 1.0.1645245

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 (67) hide show
  1. package/front_end/core/host/UserMetrics.ts +3 -2
  2. package/front_end/core/sdk/CSSPropertyParserMatchers.ts +2 -3
  3. package/front_end/core/sdk/NetworkRequest.ts +0 -1
  4. package/front_end/entrypoints/heap_snapshot_worker/HeapSnapshot.ts +37 -0
  5. package/front_end/generated/SupportedCSSProperties.js +4 -2
  6. package/front_end/models/ai_assistance/AiAgent2.ts +23 -13
  7. package/front_end/models/ai_assistance/README.md +5 -4
  8. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +15 -0
  9. package/front_end/models/ai_assistance/agents/README.md +57 -0
  10. package/front_end/models/ai_assistance/agents/StylingAgent.ts +26 -3
  11. package/front_end/models/ai_assistance/tools/ExecuteJavaScript.ts +9 -12
  12. package/front_end/models/ai_assistance/tools/GetStyles.ts +19 -12
  13. package/front_end/models/ai_assistance/tools/README.md +45 -0
  14. package/front_end/models/ai_assistance/tools/Tool.ts +74 -11
  15. package/front_end/models/ai_assistance/tools/ToolRegistry.ts +21 -5
  16. package/front_end/models/heap_snapshot/HeapSnapshotModel.ts +18 -2
  17. package/front_end/models/heap_snapshot/HeapSnapshotProxy.ts +4 -0
  18. package/front_end/models/stack_trace/DetailedErrorStackParser.ts +17 -4
  19. package/front_end/models/stack_trace/StackTraceModel.ts +34 -1
  20. package/front_end/models/trace/Styles.ts +29 -7
  21. package/front_end/models/trace/handlers/PageLoadMetricsHandler.ts +33 -4
  22. package/front_end/models/trace/helpers/Timing.ts +10 -0
  23. package/front_end/models/trace/types/TraceEvents.ts +22 -2
  24. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +8 -2
  25. package/front_end/panels/ai_assistance/ai_assistance-meta.ts +16 -0
  26. package/front_end/panels/application/ApplicationPanelSidebar.ts +44 -0
  27. package/front_end/panels/application/ServiceWorkersView.ts +2 -2
  28. package/front_end/panels/application/WebMCPView.ts +0 -1
  29. package/front_end/panels/application/components/BackForwardCacheView.ts +1 -2
  30. package/front_end/panels/console/ConsoleView.ts +6 -1
  31. package/front_end/panels/console/ConsoleViewMessage.ts +46 -213
  32. package/front_end/panels/console/SymbolizedErrorWidget.ts +4 -1
  33. package/front_end/panels/elements/AdoptedStyleSheetTreeElement.ts +0 -1
  34. package/front_end/panels/elements/ElementsTreeElement.ts +0 -2
  35. package/front_end/panels/elements/PropertyRenderer.ts +0 -1
  36. package/front_end/panels/elements/StylesSidebarPane.ts +9 -2
  37. package/front_end/panels/issues/AffectedResourcesView.ts +1 -1
  38. package/front_end/panels/issues/AffectedSourcesView.ts +1 -1
  39. package/front_end/panels/lighthouse/LighthouseReportRenderer.ts +0 -1
  40. package/front_end/panels/mobile_throttling/ThrottlingSettingsTab.ts +10 -0
  41. package/front_end/panels/network/NetworkDataGridNode.ts +1 -2
  42. package/front_end/panels/network/NetworkLogView.ts +34 -7
  43. package/front_end/panels/profiler/HeapProfileView.ts +0 -1
  44. package/front_end/panels/profiler/HeapSnapshotGridNodes.ts +0 -1
  45. package/front_end/panels/profiler/HeapSnapshotView.ts +1 -1
  46. package/front_end/panels/settings/components/SyncSection.ts +1 -1
  47. package/front_end/panels/timeline/TimelineFlameChartView.ts +5 -4
  48. package/front_end/panels/timeline/TimelinePanel.ts +7 -0
  49. package/front_end/panels/timeline/TimelineUIUtils.ts +13 -14
  50. package/front_end/panels/timeline/TimingsTrackAppender.ts +7 -5
  51. package/front_end/panels/timeline/components/LayoutShiftDetails.ts +0 -1
  52. package/front_end/panels/timeline/components/NetworkRequestDetails.ts +0 -2
  53. package/front_end/panels/timeline/components/insights/ForcedReflow.ts +0 -1
  54. package/front_end/panels/timeline/components/insights/NodeLink.ts +0 -1
  55. package/front_end/panels/timeline/overlays/OverlaysImpl.ts +2 -0
  56. package/front_end/third_party/chromium/README.chromium +1 -1
  57. package/front_end/ui/helpers/OpenInNewTab.ts +3 -3
  58. package/front_end/ui/kit/link/Link.ts +16 -2
  59. package/front_end/ui/legacy/InspectorDrawerView.ts +14 -5
  60. package/front_end/ui/legacy/InspectorView.ts +4 -1
  61. package/front_end/ui/legacy/PlusButton.ts +6 -1
  62. package/front_end/ui/legacy/Widget.ts +19 -1
  63. package/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts +95 -31
  64. package/front_end/ui/legacy/components/utils/JSPresentationUtils.ts +0 -1
  65. package/front_end/ui/legacy/components/utils/Linkifier.ts +2 -16
  66. package/front_end/ui/visual_logging/KnownContextValues.ts +5 -0
  67. package/package.json +1 -1
@@ -9,22 +9,76 @@ import type {executeJsCode} from '../agents/ExecuteJavascript.js';
9
9
  import type {ChangeManager} from '../ChangeManager.js';
10
10
 
11
11
  /**
12
- * Context provided to the tool's handler execution.
12
+ * Base capability for all tool contexts, providing access to the conversation context.
13
13
  */
14
- export interface ToolContext {
14
+ export interface BaseToolCapability {
15
+ /**
16
+ * The active context for the current conversation step, if any.
17
+ */
15
18
  conversationContext: ConversationContext<unknown>|null;
16
- changeManager?: ChangeManager;
17
- createExtensionScope?: (changes: ChangeManager) => {
18
- install(): Promise<void>, uninstall(): Promise<void>,
19
- };
20
- execJs?: typeof executeJsCode;
19
+ }
20
+
21
+ /**
22
+ * Capability for tools that need to execute JavaScript code on the inspected page.
23
+ */
24
+ export interface PageExecutionCapability {
25
+ /**
26
+ * Function to execute JavaScript code in the page context.
27
+ */
28
+ execJs: typeof executeJsCode;
29
+
21
30
  /**
22
31
  * Returns the DOM node that acts as the execution context (i.e. `$0` inside the execution context)
23
32
  * for running JavaScript.
24
33
  */
25
- getExecutionContextNode?: () => SDK.DOMModel.DOMNode | null;
34
+ getExecutionContextNode(): SDK.DOMModel.DOMNode|null;
26
35
  }
27
36
 
37
+ /**
38
+ * Capability for tools that need to manage and apply style mutations to the page.
39
+ */
40
+ export interface StyleMutationCapability {
41
+ /**
42
+ * The change manager for tracking and applying style changes.
43
+ */
44
+ changeManager: ChangeManager;
45
+
46
+ /**
47
+ * Creates an extension scope for applying changes, ensuring they can be uninstalled when done.
48
+ */
49
+ createExtensionScope(changes: ChangeManager): {
50
+ install(): Promise<void>,
51
+ uninstall(): Promise<void>,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Capability for tools that need access to the current SDK Target of the inspected page.
57
+ */
58
+ export interface TargetCapability {
59
+ /**
60
+ * Returns the current SDK Target for the inspected page.
61
+ */
62
+ getTarget(): SDK.Target.Target|null;
63
+ }
64
+
65
+ /**
66
+ * Capability for tools that need to enforce origin locking for security.
67
+ */
68
+ export interface OriginLockCapability {
69
+ /**
70
+ * Returns the origin that the current conversation is locked to, if any.
71
+ */
72
+ getEstablishedOrigin(): string|undefined;
73
+ }
74
+
75
+ /**
76
+ * Unified context interface providing all capabilities available in the project.
77
+ * Used by the agent to pass a complete context to any tool type-safely.
78
+ */
79
+ export type AllToolsContext =
80
+ BaseToolCapability&PageExecutionCapability&StyleMutationCapability&TargetCapability&OriginLockCapability;
81
+
28
82
  /**
29
83
  * Base argument type for AI Tools.
30
84
  */
@@ -50,21 +104,30 @@ export interface BaseTool {
50
104
 
51
105
  /**
52
106
  * Main generic interface for defining a Tool.
53
- * Binds the parameter schema properties and the handler implementation to a strict `Args` contract.
107
+ * Binds the parameter schema properties and the handler implementation to a strict `Args` and `ContextType` contract.
54
108
  *
55
109
  * @template Args - The expected object type for tool arguments. Must be an object type.
56
110
  * @template ReturnType - The type of data returned by the handler function.
111
+ * @template ContextType - The interface defining the capabilities this tool requires. Defaults to `BaseToolCapability`.
57
112
  */
58
- export interface Tool<Args extends ToolArgs = ToolArgs, ReturnType = unknown, > extends BaseTool {
113
+ export interface Tool<Args extends ToolArgs = ToolArgs, ReturnType = unknown,
114
+ ContextType extends BaseToolCapability = BaseToolCapability> extends BaseTool {
59
115
  readonly parameters: Host.AidaClient.FunctionObjectParam<keyof Args>;
60
116
  readonly displayInfoFromArgs?: (
61
117
  args: Args,
62
118
  ) => {
63
119
  title?: string, thought?: string, action?: string, suggestions?: [string, ...string[]],
64
120
  };
121
+ /**
122
+ * The implementation function called when the AI invokes this tool.
123
+ *
124
+ * @param args The arguments provided by the AI model matching the tool's parameter schema.
125
+ * @param context The context object providing the capabilities requested by `ContextType`.
126
+ * @param options Additional runtime options for the handler execution.
127
+ */
65
128
  handler(
66
129
  args: Args,
67
- context: ToolContext,
130
+ context: ContextType,
68
131
  options?: FunctionHandlerOptions,
69
132
  ): Promise<FunctionCallHandlerResult<ReturnType>>;
70
133
  }
@@ -4,11 +4,15 @@
4
4
 
5
5
  import {ExecuteJavaScriptTool} from './ExecuteJavaScript.js';
6
6
  import {GetStylesTool} from './GetStyles.js';
7
- import {type Tool, ToolName} from './Tool.js';
7
+ import {type AllToolsContext, type Tool, type ToolArgs, ToolName} from './Tool.js';
8
8
 
9
9
  /**
10
10
  * Plain object registry containing concrete instantiated tools.
11
- * Keep this type concrete (no type-erasure) to preserve exact tool types.
11
+ *
12
+ * This object is deliberately declared as a plain object without an explicit type annotation
13
+ * (like `Record<ToolName, Tool>`) to preserve the exact concrete type of each registered tool.
14
+ * This is required to support compile-time type safety and inference in the overloaded
15
+ * `ToolRegistry.get()` method, which maps a literal `ToolName` key to its specific class type.
12
16
  */
13
17
  export const TOOLS = {
14
18
  [ToolName.EXECUTE_JAVASCRIPT]: new ExecuteJavaScriptTool(),
@@ -23,14 +27,26 @@ export class ToolRegistry {
23
27
  * Retrieves a tool by its literal name with 100% type safety.
24
28
  *
25
29
  * @template K - A key from the `TOOLS` registry.
30
+ * @param name The literal name of the tool to retrieve.
26
31
  * @returns The concrete class type of the requested tool.
27
32
  */
28
33
  static get<K extends keyof typeof TOOLS>(name: K): typeof TOOLS[K];
29
34
  /**
30
35
  * Fallback retrieval signature for general or runtime string lookups.
36
+ *
37
+ * @param name The string name of the tool to retrieve, used when the tool name is only known at runtime.
38
+ * @returns The generic Tool interface, or undefined if not found.
31
39
  */
32
- static get(name: string): Tool|undefined;
33
- static get(name: string): Tool|undefined {
34
- return Object.prototype.hasOwnProperty.call(TOOLS, name) ? TOOLS[name as keyof typeof TOOLS] as Tool : undefined;
40
+ static get(name: string): Tool<ToolArgs, unknown, AllToolsContext>|undefined;
41
+ static get(name: string): Tool<ToolArgs, unknown, AllToolsContext>|undefined {
42
+ // We use a double assertion (`as unknown as Tool<...>`) here. TypeScript's variance
43
+ // rules prevent direct casting from specific concrete tools (which have narrowed,
44
+ // capability-specific contexts) to the generic `Tool` signature that uses `AllToolsContext`.
45
+ // This cast is runtime-safe because any capability requested by a specific tool is
46
+ // guaranteed to be satisfied by `AllToolsContext`, and the handler will only access
47
+ // the capabilities it expects.
48
+ return Object.prototype.hasOwnProperty.call(TOOLS, name) ?
49
+ TOOLS[name as keyof typeof TOOLS] as unknown as Tool<ToolArgs, unknown, AllToolsContext>:
50
+ undefined;
35
51
  }
36
52
  }
@@ -136,10 +136,16 @@ export class Diff {
136
136
  removedCount = 0;
137
137
  addedSize = 0;
138
138
  removedSize = 0;
139
- deletedIndexes: number[] = [];
140
- addedIndexes: number[] = [];
141
139
  countDelta!: number;
142
140
  sizeDelta!: number;
141
+ // Data about added nodes
142
+ addedIndexes: number[] = [];
143
+ addedIds: number[] = [];
144
+ addedSelfSizes: number[] = [];
145
+ // Data about deleted nodes
146
+ deletedIndexes: number[] = [];
147
+ deletedIds: number[] = [];
148
+ deletedSelfSizes: number[] = [];
143
149
  constructor(name: string) {
144
150
  this.name = name;
145
151
  }
@@ -287,3 +293,13 @@ export interface RetainingPaths {
287
293
  siblings?: boolean,
288
294
  };
289
295
  }
296
+
297
+ export interface DominatorNode {
298
+ nodeId: number;
299
+ nodeIndex: number;
300
+ nodeName: string;
301
+ retainedSize: number;
302
+ selfSize: number;
303
+ }
304
+
305
+ export type DominatorChain = DominatorNode[];
@@ -370,6 +370,10 @@ export class HeapSnapshotProxy extends HeapSnapshotProxyObject {
370
370
  return this.callMethodPromise('getRetainingPaths', nodeIndex, maxDepth, maxNodes, maxSiblings);
371
371
  }
372
372
 
373
+ getDominatorsOf(nodeIndex: number): Promise<HeapSnapshotModel.DominatorChain> {
374
+ return this.callMethodPromise('getDominatorsOf', nodeIndex);
375
+ }
376
+
373
377
  unignoreNodeInRetainersView(nodeIndex: number): Promise<void> {
374
378
  return this.callMethodPromise('unignoreNodeInRetainersView', nodeIndex);
375
379
  }
@@ -10,13 +10,15 @@ import type {RawFrame} from './Trie.js';
10
10
 
11
11
  const CALL_FRAME_REGEX = /^\s*at\s+/;
12
12
 
13
+ export type ResolveURLCallback = (url: Platform.DevToolsPath.UrlString) => Platform.DevToolsPath.UrlString|null;
14
+
13
15
  /**
14
16
  * Takes a V8 Error#stack string and extracts structured information.
15
17
  *
16
18
  * @returns Null if the provided string has an unexpected format. A
17
19
  * populated `RawFrame[]` otherwise.
18
20
  */
19
- export function parseRawFramesFromErrorStack(stack: string): RawFrame[]|null {
21
+ export function parseRawFramesFromErrorStack(stack: string, resolveURL?: ResolveURLCallback): RawFrame[]|null {
20
22
  const lines = stack.split('\n');
21
23
  const firstAtLineIndex = findFramesStartLine(lines);
22
24
  const rawFrames: RawFrame[] = [];
@@ -66,6 +68,8 @@ export function parseRawFramesFromErrorStack(stack: string): RawFrame[]|null {
66
68
  if (lineContent.endsWith(')') && openParenIndex !== -1) {
67
69
  functionName = lineContent.substring(0, openParenIndex).trim();
68
70
  location = lineContent.substring(openParenIndex + 2, lineContent.length - 1);
71
+ } else if (lineContent.startsWith('(') && lineContent.endsWith(')')) {
72
+ location = lineContent.substring(1, lineContent.length - 1);
69
73
  } else {
70
74
  location = lineContent;
71
75
  }
@@ -90,9 +94,9 @@ export function parseRawFramesFromErrorStack(stack: string): RawFrame[]|null {
90
94
  if (innerOpenParen !== -1) {
91
95
  evalFunctionName = evalOriginStr.substring(0, innerOpenParen).trim();
92
96
  evalLocation = evalOriginStr.substring(innerOpenParen + 2, evalOriginStr.length - 1);
93
- evalOrigin = parseRawFramesFromErrorStack(` at ${evalFunctionName} (${evalLocation})`)?.[0];
97
+ evalOrigin = parseRawFramesFromErrorStack(` at ${evalFunctionName} (${evalLocation})`, resolveURL)?.[0];
94
98
  } else {
95
- evalOrigin = parseRawFramesFromErrorStack(` at ${evalFunctionName}`)?.[0];
99
+ evalOrigin = parseRawFramesFromErrorStack(` at ${evalFunctionName}`, resolveURL)?.[0];
96
100
  }
97
101
  }
98
102
 
@@ -112,9 +116,18 @@ export function parseRawFramesFromErrorStack(stack: string): RawFrame[]|null {
112
116
  }
113
117
  } else if (location) {
114
118
  const splitResult = Common.ParsedURL.ParsedURL.splitLineAndColumn(location);
115
- url = splitResult.url;
116
119
  lineNumber = splitResult.lineNumber ?? -1;
117
120
  columnNumber = splitResult.columnNumber ?? -1;
121
+
122
+ if (resolveURL && splitResult.url !== '<anonymous>' && splitResult.url !== 'native') {
123
+ const resolved = resolveURL(splitResult.url);
124
+ if (!resolved) {
125
+ return null;
126
+ }
127
+ url = resolved;
128
+ } else {
129
+ url = splitResult.url;
130
+ }
118
131
  }
119
132
 
120
133
  // Handle "typeName.methodName [as alias]"
@@ -3,6 +3,7 @@
3
3
  // found in the LICENSE file.
4
4
 
5
5
  import * as Common from '../../core/common/common.js';
6
+ import type * as Platform from '../../core/platform/platform.js';
6
7
  import * as SDK from '../../core/sdk/sdk.js';
7
8
  import type * as Protocol from '../../generated/protocol.js';
8
9
 
@@ -65,7 +66,17 @@ export class StackTraceModel extends SDK.SDKModel.SDKModel<unknown> {
65
66
  async createFromErrorStackLikeString(
66
67
  stack: string, rawFramesToUIFrames: TranslateRawFrames,
67
68
  exceptionDetails?: Protocol.Runtime.ExceptionDetails): Promise<StackTrace.StackTrace.ParsedErrorStackTrace|null> {
68
- const rawFrames = parseRawFramesFromErrorStack(stack);
69
+ const debuggerModel = this.target().model(SDK.DebuggerModel.DebuggerModel) as SDK.DebuggerModel.DebuggerModel;
70
+ const baseURL = this.target().inspectedURL();
71
+ const resolveURL = (url: Platform.DevToolsPath.UrlString): Platform.DevToolsPath.UrlString|null => {
72
+ let urlWithScheme = parseOrScriptMatch(debuggerModel, url);
73
+ if (!urlWithScheme && Common.ParsedURL.ParsedURL.isRelativeURL(url)) {
74
+ urlWithScheme = parseOrScriptMatch(debuggerModel, Common.ParsedURL.ParsedURL.completeURL(baseURL, url));
75
+ }
76
+ return urlWithScheme;
77
+ };
78
+
79
+ const rawFrames = parseRawFramesFromErrorStack(stack, resolveURL);
69
80
  if (!rawFrames) {
70
81
  return null;
71
82
  }
@@ -267,4 +278,26 @@ async function translateEvalOrigin(
267
278
  return new EvalOrigin(frames, parentEvalOrigin);
268
279
  }
269
280
 
281
+ function parseOrScriptMatch(debuggerModel: SDK.DebuggerModel.DebuggerModel,
282
+ url: Platform.DevToolsPath.UrlString|null): Platform.DevToolsPath.UrlString|null {
283
+ if (!url) {
284
+ return null;
285
+ }
286
+ if (Common.ParsedURL.ParsedURL.isValidUrlString(url)) {
287
+ return url;
288
+ }
289
+ if (debuggerModel.scriptsForSourceURL(url).length) {
290
+ return url;
291
+ }
292
+ // nodejs stack traces contain (absolute) file paths, but v8 reports them as file: urls.
293
+ try {
294
+ const fileUrl = new URL(url, 'file://');
295
+ if (debuggerModel.scriptsForSourceURL(fileUrl.href as Platform.DevToolsPath.UrlString).length) {
296
+ return fileUrl.href as Platform.DevToolsPath.UrlString;
297
+ }
298
+ } catch {
299
+ }
300
+ return null;
301
+ }
302
+
270
303
  SDK.SDKModel.SDKModel.register(StackTraceModel, {capabilities: SDK.Target.Capability.NONE, autostart: false});
@@ -291,6 +291,10 @@ const UIStrings = {
291
291
  * @description Text in Timeline UIUtils of the Performance panel
292
292
  */
293
293
  frameStartedLoading: 'Frame started loading',
294
+ /**
295
+ * @description Text in Timeline UIUtils of the Performance panel
296
+ */
297
+ softNavigationStart: 'Soft navigation start',
294
298
  /**
295
299
  * @description Text in Timeline UIUtils of the Performance panel
296
300
  */
@@ -307,6 +311,10 @@ const UIStrings = {
307
311
  * @description Text in Timeline UIUtils of the Performance panel
308
312
  */
309
313
  firstContentfulPaint: 'First Contentful Paint',
314
+ /**
315
+ * @description Text in Timeline UIUtils of the Performance panel
316
+ */
317
+ softFirstContentfulPaint: 'Soft First Contentful Paint',
310
318
  /**
311
319
  * @description Text in Timeline UIUtils of the Performance panel
312
320
  */
@@ -866,6 +874,12 @@ export function maybeInitSylesMap(): EventStylesMap {
866
874
  true,
867
875
  ),
868
876
 
877
+ [Types.Events.Name.SOFT_NAVIGATION_START]: new TimelineRecordStyle(
878
+ i18nString(UIStrings.softNavigationStart),
879
+ defaultCategoryStyles.loading,
880
+ true,
881
+ ),
882
+
869
883
  [Types.Events.Name.MARK_FIRST_PAINT]: new TimelineRecordStyle(
870
884
  i18nString(UIStrings.firstPaint),
871
885
  defaultCategoryStyles.painting,
@@ -878,6 +892,12 @@ export function maybeInitSylesMap(): EventStylesMap {
878
892
  true,
879
893
  ),
880
894
 
895
+ [Types.Events.Name.MARK_SOFT_FCP]: new TimelineRecordStyle(
896
+ i18nString(UIStrings.softFirstContentfulPaint),
897
+ defaultCategoryStyles.rendering,
898
+ true,
899
+ ),
900
+
881
901
  [Types.Events.Name.MARK_LCP_CANDIDATE]: new TimelineRecordStyle(
882
902
  i18nString(UIStrings.largestContentfulPaint),
883
903
  defaultCategoryStyles.rendering,
@@ -1024,11 +1044,11 @@ export function maybeInitSylesMap(): EventStylesMap {
1024
1044
  [Types.Events.Name.ASYNC_TASK]:
1025
1045
  new TimelineRecordStyle(i18nString(UIStrings.asyncTask), defaultCategoryStyles.async),
1026
1046
 
1027
- [Types.Events.Name.LAYOUT_SHIFT]: new TimelineRecordStyle(
1028
- i18nString(UIStrings.layoutShift), defaultCategoryStyles.experience,
1029
- /* Mark LayoutShifts as hidden; in the timeline we render
1030
- * SyntheticLayoutShifts so those are the ones visible to the user */
1031
- true),
1047
+ [Types.Events.Name.LAYOUT_SHIFT]:
1048
+ new TimelineRecordStyle(i18nString(UIStrings.layoutShift), defaultCategoryStyles.experience,
1049
+ /* Mark LayoutShifts as hidden; in the timeline we render
1050
+ * SyntheticLayoutShifts so those are the ones visible to the user */
1051
+ true),
1032
1052
 
1033
1053
  [Types.Events.Name.SYNTHETIC_LAYOUT_SHIFT]:
1034
1054
  new TimelineRecordStyle(i18nString(UIStrings.layoutShift), defaultCategoryStyles.experience),
@@ -1123,9 +1143,11 @@ export function markerDetailsForEvent(event: Types.Events.Event): {
1123
1143
  } {
1124
1144
  let title = '';
1125
1145
  let color = 'var(--color-text-primary)';
1126
- if (Types.Events.isFirstContentfulPaint(event)) {
1146
+ if (Types.Events.isAnyFirstContentfulPaint(event)) {
1127
1147
  color = 'var(--sys-color-green-bright)';
1128
- title = Handlers.ModelHandlers.PageLoadMetrics.MetricName.FCP;
1148
+ title = (Types.Events.isSoftFirstContentfulPaint(event)) ?
1149
+ Handlers.ModelHandlers.PageLoadMetrics.MetricName.SOFT_FCP :
1150
+ Handlers.ModelHandlers.PageLoadMetrics.MetricName.FCP;
1129
1151
  }
1130
1152
  if (Types.Events.isAnyLargestContentfulPaintCandidate(event)) {
1131
1153
  color = 'var(--sys-color-green)';
@@ -75,6 +75,28 @@ export function handleEvent(event: Types.Events.Event): void {
75
75
  return;
76
76
  }
77
77
  pageLoadEventsArray.push(event);
78
+
79
+ // A soft nav entry includes the Soft FCP details but we want to process both
80
+ // so push a separate Soft FCP event
81
+ if (Types.Events.isSoftNavigationStart(event) && event.args?.context?.firstContentfulPaint) {
82
+ const syntheticSoftFcpEvent = Helpers.SyntheticEvents.SyntheticEventsManager
83
+ .registerSyntheticEvent<Types.Events.SyntheticSoftFirstContentfulPaint>({
84
+ name: Types.Events.Name.MARK_SOFT_FCP,
85
+ ph: Types.Events.Phase.MARK,
86
+ rawSourceEvent: event,
87
+ pid: event.pid,
88
+ tid: event.tid,
89
+ ts: Types.Timing.Micro(event.args.context.firstContentfulPaint),
90
+ cat: event.cat,
91
+ args: {
92
+ frame: event.args.frame,
93
+ context: {
94
+ ...event.args.context,
95
+ },
96
+ },
97
+ });
98
+ pageLoadEventsArray.push(syntheticSoftFcpEvent);
99
+ }
78
100
  }
79
101
 
80
102
  function storePageLoadMetricAgainstNavigationId(
@@ -101,7 +123,7 @@ function storePageLoadMetricAgainstNavigationId(
101
123
  return;
102
124
  }
103
125
 
104
- if (Types.Events.isFirstContentfulPaint(event)) {
126
+ if (Types.Events.isAnyFirstContentfulPaint(event)) {
105
127
  const fcpTime = Types.Timing.Micro(event.ts - navigation.ts);
106
128
  const classification = scoreClassificationForFirstContentfulPaint(fcpTime);
107
129
  const metricScore = {event, metricName: MetricName.FCP, classification, navigation, timing: fcpTime};
@@ -226,7 +248,7 @@ function storeMetricScore(frameId: string, navigation: AnyNavigationStart, metri
226
248
  }
227
249
 
228
250
  export function getFrameIdForPageLoadEvent(event: Types.Events.PageLoadEvent): string {
229
- if (Types.Events.isFirstContentfulPaint(event) || Types.Events.isInteractiveTime(event) ||
251
+ if (Types.Events.isAnyFirstContentfulPaint(event) || Types.Events.isInteractiveTime(event) ||
230
252
  Types.Events.isAnyLargestContentfulPaintCandidate(event) || Types.Events.isNavigationStart(event) ||
231
253
  Types.Events.isSoftNavigationStart(event) || Types.Events.isLayoutShift(event) ||
232
254
  Types.Events.isFirstPaint(event)) {
@@ -243,7 +265,7 @@ export function getFrameIdForPageLoadEvent(event: Types.Events.PageLoadEvent): s
243
265
  }
244
266
 
245
267
  function getNavigationForPageLoadEvent(event: Types.Events.PageLoadEvent): AnyNavigationStart|null {
246
- if (Types.Events.isFirstContentfulPaint(event) || Types.Events.isAnyLargestContentfulPaintCandidate(event) ||
268
+ if (Types.Events.isAnyFirstContentfulPaint(event) || Types.Events.isAnyLargestContentfulPaintCandidate(event) ||
247
269
  Types.Events.isFirstPaint(event)) {
248
270
  const {navigationsByNavigationId, softNavigationsById} = metaHandlerData();
249
271
 
@@ -255,6 +277,12 @@ function getNavigationForPageLoadEvent(event: Types.Events.PageLoadEvent): AnyNa
255
277
  // The most recent soft navigation must have been before the trace started.
256
278
  return null;
257
279
  }
280
+ } else if (Types.Events.isSoftFirstContentfulPaint(event) && event.args.context?.performanceTimelineNavigationId) {
281
+ navigation = softNavigationsById.get(event.args.context.performanceTimelineNavigationId);
282
+ if (!navigation) {
283
+ // The most recent soft navigation must have been before the trace started.
284
+ return null;
285
+ }
258
286
  } else {
259
287
  const navigationId = event.args.data?.navigationId;
260
288
  if (!navigationId) {
@@ -428,7 +456,7 @@ export async function finalize(): Promise<void> {
428
456
  // Filter out LCP candidates to use only definitive LCP values
429
457
  const allEventsButLCP =
430
458
  pageLoadEventsArray.filter(event => !Types.Events.isAnyLargestContentfulPaintCandidate(event));
431
- const markerEvents = [...allFinalLCPEvents, ...allEventsButLCP].filter(Types.Events.isMarkerEvent);
459
+ const markerEvents = [...allEventsButLCP, ...allFinalLCPEvents].filter(Types.Events.isMarkerEvent);
432
460
  // Filter by main frame and sort.
433
461
  allMarkerEvents =
434
462
  markerEvents.filter(event => getFrameIdForPageLoadEvent(event) === mainFrame).sort((a, b) => a.ts - b.ts);
@@ -495,6 +523,7 @@ export const enum MetricName {
495
523
  NAV = 'Nav',
496
524
  // Soft Navigation and Soft Metrics
497
525
  SOFT_NAV = 'Nav*',
526
+ SOFT_FCP = 'FCP*',
498
527
  SOFT_LCP = 'LCP*',
499
528
  // Note: INP is handled in UserInteractionsHandler
500
529
  }
@@ -32,6 +32,16 @@ export function timeStampForEventAdjustedByClosestNavigation(
32
32
  if (navigationForEvent) {
33
33
  eventTimeStamp = event.ts - navigationForEvent.ts;
34
34
  }
35
+ } else if (Types.Events.isSoftFirstContentfulPaint(event) && event.args?.context?.performanceTimelineNavigationId) {
36
+ const navigationForEvent = softNavigationsById.get(event.args.context.performanceTimelineNavigationId);
37
+ if (navigationForEvent) {
38
+ eventTimeStamp = event.ts - navigationForEvent.ts;
39
+ }
40
+ } else if (Types.Events.isSoftNavigationStart(event)) {
41
+ const navigationForEvent = getNavigationForTraceEvent(event, event.args.frame, navigationsByFrameId);
42
+ if (navigationForEvent) {
43
+ eventTimeStamp = event.ts - navigationForEvent.ts;
44
+ }
35
45
  } else if (event.args?.data?.navigationId) {
36
46
  const navigationForEvent = navigationsByNavigationId.get(event.args.data.navigationId);
37
47
  if (navigationForEvent) {
@@ -737,6 +737,16 @@ export interface FirstContentfulPaint extends Mark {
737
737
  };
738
738
  }
739
739
 
740
+ // Soft FCP is basically a copy of SoftNavigationStart but with a different name
741
+ // and a different ts.
742
+ export interface SyntheticSoftFirstContentfulPaint extends Omit<SoftNavigationStart, 'name'|'ph'>,
743
+ Omit<SyntheticBased, 'name'|'ph'|'args'> {
744
+ name: Name.MARK_SOFT_FCP;
745
+ ph: Phase.MARK;
746
+ }
747
+
748
+ export type AnyFirstContentfulPaint = FirstContentfulPaint|SyntheticSoftFirstContentfulPaint;
749
+
740
750
  export interface FirstPaint extends Mark {
741
751
  name: Name.MARK_FIRST_PAINT;
742
752
  args: Args&{
@@ -747,14 +757,14 @@ export interface FirstPaint extends Mark {
747
757
  };
748
758
  }
749
759
 
750
- export type PageLoadEvent = FirstContentfulPaint|MarkDOMContent|InteractiveTime|AnyLargestContentfulPaintCandidate|
760
+ export type PageLoadEvent = AnyFirstContentfulPaint|MarkDOMContent|InteractiveTime|AnyLargestContentfulPaintCandidate|
751
761
  LayoutShift|FirstPaint|MarkLoad|NavigationStart|SoftNavigationStart;
752
762
 
753
763
  const markerTypeGuards = [
754
764
  isMarkDOMContent,
755
765
  isMarkLoad,
756
766
  isFirstPaint,
757
- isFirstContentfulPaint,
767
+ isAnyFirstContentfulPaint,
758
768
  isAnyLargestContentfulPaintCandidate,
759
769
  isNavigationStart,
760
770
  isSoftNavigationStart,
@@ -765,6 +775,7 @@ export const MarkerName = [
765
775
  Name.MARK_LOAD,
766
776
  Name.MARK_FIRST_PAINT,
767
777
  Name.MARK_FCP,
778
+ Name.MARK_SOFT_FCP,
768
779
  Name.MARK_LCP_CANDIDATE,
769
780
  Name.MARK_LCP_CANDIDATE_FOR_SOFT_NAVIGATION,
770
781
  Name.NAVIGATION_START,
@@ -2285,6 +2296,14 @@ export function isFirstContentfulPaint(event: Event): event is FirstContentfulPa
2285
2296
  return event.name === Name.MARK_FCP;
2286
2297
  }
2287
2298
 
2299
+ export function isSoftFirstContentfulPaint(event: Event): event is SyntheticSoftFirstContentfulPaint {
2300
+ return event.name === Name.MARK_SOFT_FCP;
2301
+ }
2302
+
2303
+ export function isAnyFirstContentfulPaint(event: Event): event is AnyFirstContentfulPaint {
2304
+ return event.name === Name.MARK_FCP || event.name === Name.MARK_SOFT_FCP;
2305
+ }
2306
+
2288
2307
  export function isAnyLargestContentfulPaintCandidate(event: Event): event is AnyLargestContentfulPaintCandidate {
2289
2308
  return event.name === Name.MARK_LCP_CANDIDATE || event.name === Name.MARK_LCP_CANDIDATE_FOR_SOFT_NAVIGATION;
2290
2309
  }
@@ -3168,6 +3187,7 @@ export const enum Name {
3168
3187
  MARK_DOM_CONTENT = 'MarkDOMContent',
3169
3188
  MARK_FIRST_PAINT = 'firstPaint',
3170
3189
  MARK_FCP = 'firstContentfulPaint',
3190
+ MARK_SOFT_FCP = 'SyntheticSoftFirstContentfulPaint',
3171
3191
  MARK_LCP_CANDIDATE = 'largestContentfulPaint::Candidate',
3172
3192
  MARK_LCP_CANDIDATE_FOR_SOFT_NAVIGATION = 'largestContentfulPaint::CandidateForSoftNavigation',
3173
3193
  MARK_LCP_INVALIDATE = 'largestContentfulPaint::Invalidate',
@@ -1575,7 +1575,12 @@ export class AiAssistancePanel extends UI.Panel.Panel {
1575
1575
  break;
1576
1576
  }
1577
1577
  case 'ai-assistance.storage-floating-button': {
1578
- Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiAssistanceOpenedFromStoragePanelFloatingButton);
1578
+ Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiAssistanceOpenedFromApplicationPanelFloatingButton);
1579
+ targetConversationType = AiAssistanceModel.AiHistoryStorage.ConversationType.STORAGE;
1580
+ break;
1581
+ }
1582
+ case 'ai-assistance.application-panel-context': {
1583
+ Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiAssistanceOpenedFromApplicationPanel);
1579
1584
  targetConversationType = AiAssistanceModel.AiHistoryStorage.ConversationType.STORAGE;
1580
1585
  break;
1581
1586
  }
@@ -2116,7 +2121,8 @@ export class ActionDelegate implements UI.ActionRegistration.ActionDelegate {
2116
2121
  case 'drjones.performance-panel-context':
2117
2122
  case 'drjones.sources-floating-button':
2118
2123
  case 'drjones.sources-panel-context':
2119
- case 'ai-assistance.storage-floating-button': {
2124
+ case 'ai-assistance.storage-floating-button':
2125
+ case 'ai-assistance.application-panel-context': {
2120
2126
  void (async () => {
2121
2127
  const view = UI.ViewManager.ViewManager.instance().view(
2122
2128
  AiAssistancePanel.panelName,
@@ -313,3 +313,19 @@ UI.ActionRegistration.registerActionExtension({
313
313
  condition: config =>
314
314
  isStorageAgentFeatureAvailable(config) && !isPolicyRestricted(config) && !isGeoRestricted(config),
315
315
  });
316
+
317
+ UI.ActionRegistration.registerActionExtension({
318
+ actionId: 'ai-assistance.application-panel-context',
319
+ contextTypes(): [] {
320
+ return [];
321
+ },
322
+ category: UI.ActionRegistration.ActionCategory.GLOBAL,
323
+ title: i18nAiBrandedString(UIStrings.debugWithGemini, UIStrings.debugWithAi),
324
+ configurableBindings: false,
325
+ async loadActionDelegate() {
326
+ const AiAssistance = await loadAiAssistanceModule();
327
+ return new AiAssistance.ActionDelegate();
328
+ },
329
+ condition: config =>
330
+ isStorageAgentFeatureAvailable(config) && !isPolicyRestricted(config) && !isGeoRestricted(config),
331
+ });