chrome-devtools-frontend 1.0.1605219 → 1.0.1605390

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 (36) hide show
  1. package/front_end/Images/src/dots-circle.svg +10 -0
  2. package/front_end/core/sdk/WebMCPModel.ts +57 -2
  3. package/front_end/models/ai_assistance/agents/AiAgent.ts +9 -1
  4. package/front_end/models/ai_assistance/agents/ConversationSummaryAgent.ts +10 -9
  5. package/front_end/models/ai_assistance/agents/ExecuteJavascript.ts +301 -0
  6. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +19 -8
  7. package/front_end/models/ai_assistance/agents/StylingAgent.ts +12 -264
  8. package/front_end/panels/ai_assistance/components/ChatMessage.ts +132 -48
  9. package/front_end/panels/ai_assistance/components/ChatView.ts +2 -13
  10. package/front_end/panels/ai_assistance/components/ExportForAgentsDialog.ts +1 -1
  11. package/front_end/panels/ai_assistance/components/chatMessage.css +43 -1
  12. package/front_end/panels/ai_assistance/components/chatView.css +0 -8
  13. package/front_end/panels/application/WebMCPView.ts +363 -13
  14. package/front_end/panels/application/webMCPView.css +25 -0
  15. package/front_end/panels/elements/ElementsTreeOutline.ts +87 -0
  16. package/front_end/panels/elements/elementsTreeOutline.css +21 -0
  17. package/front_end/panels/profiler/HeapDetachedElementsView.ts +0 -4
  18. package/front_end/panels/profiler/HeapProfileView.ts +0 -4
  19. package/front_end/panels/profiler/HeapProfilerPanel.ts +4 -13
  20. package/front_end/panels/profiler/HeapSnapshotView.ts +12 -16
  21. package/front_end/panels/profiler/ProfileHeader.ts +0 -11
  22. package/front_end/panels/profiler/ProfileTypeRegistry.ts +4 -12
  23. package/front_end/panels/profiler/ProfileView.ts +1 -6
  24. package/front_end/panels/profiler/ProfilesPanel.ts +60 -7
  25. package/front_end/panels/timeline/EventsTimelineTreeView.ts +1 -1
  26. package/front_end/panels/timeline/ThirdPartyTreeView.ts +2 -2
  27. package/front_end/panels/timeline/TimelinePanel.ts +11 -0
  28. package/front_end/panels/timeline/TimelineTreeView.ts +136 -47
  29. package/front_end/panels/timeline/components/CWVMetrics.ts +18 -2
  30. package/front_end/panels/timeline/timeline-meta.ts +11 -0
  31. package/front_end/panels/timeline/utils/Helpers.ts +5 -0
  32. package/front_end/ui/legacy/FilterBar.ts +2 -0
  33. package/front_end/ui/legacy/SplitWidget.ts +33 -27
  34. package/front_end/ui/legacy/Widget.ts +36 -28
  35. package/front_end/ui/visual_logging/KnownContextValues.ts +6 -0
  36. package/package.json +1 -1
@@ -4,7 +4,6 @@
4
4
 
5
5
  import * as Host from '../../../core/host/host.js';
6
6
  import * as i18n from '../../../core/i18n/i18n.js';
7
- import * as Platform from '../../../core/platform/platform.js';
8
7
  import * as Root from '../../../core/root/root.js';
9
8
  import * as SDK from '../../../core/sdk/sdk.js';
10
9
  import * as Protocol from '../../../generated/protocol.js';
@@ -13,9 +12,8 @@ import * as Annotations from '../../annotations/annotations.js';
13
12
  import * as Emulation from '../../emulation/emulation.js';
14
13
  import {ChangeManager} from '../ChangeManager.js';
15
14
  import {debugLog} from '../debug.js';
16
- import {EvaluateAction, formatError, SideEffectError} from '../EvaluateAction.js';
17
15
  import {ExtensionScope} from '../ExtensionScope.js';
18
- import {AI_ASSISTANCE_CSS_CLASS_NAME, FREESTYLER_WORLD_NAME} from '../injected.js';
16
+ import {AI_ASSISTANCE_CSS_CLASS_NAME} from '../injected.js';
19
17
 
20
18
  import {
21
19
  type AgentOptions as BaseAgentOptions,
@@ -25,12 +23,12 @@ import {
25
23
  ConversationContext,
26
24
  type ConversationSuggestions,
27
25
  type FunctionCallHandlerResult,
28
- type FunctionHandlerOptions,
29
26
  type MultimodalInput,
30
27
  MultimodalInputType,
31
28
  type RequestOptions,
32
29
  ResponseType
33
30
  } from './AiAgent.js';
31
+ import {executeJavaScriptFunction, executeJsCode, JavascriptExecutor} from './ExecuteJavascript.js';
34
32
 
35
33
  /*
36
34
  * Strings that don't need to be translated at this time.
@@ -145,51 +143,6 @@ const MULTIMODAL_ENHANCEMENT_PROMPTS: Record<MultimodalInputType, string> = {
145
143
  [MultimodalInputType.UPLOADED_IMAGE]: promptForUploadedImage + considerationsForMultimodalInputEvaluation,
146
144
  };
147
145
 
148
- async function executeJsCode(
149
- functionDeclaration: string,
150
- {throwOnSideEffect, contextNode}: {throwOnSideEffect: boolean, contextNode: SDK.DOMModel.DOMNode|null}):
151
- Promise<string> {
152
- if (!contextNode) {
153
- throw new Error('Cannot execute JavaScript because of missing context node');
154
- }
155
- const target = contextNode.domModel().target();
156
-
157
- if (!target) {
158
- throw new Error('Target is not found for executing code');
159
- }
160
-
161
- const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
162
- const frameId = contextNode.frameId() ?? resourceTreeModel?.mainFrame?.id;
163
-
164
- if (!frameId) {
165
- throw new Error('Main frame is not found for executing code');
166
- }
167
-
168
- const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel);
169
- const pageAgent = target.pageAgent();
170
-
171
- // This returns previously created world if it exists for the frame.
172
- const {executionContextId} = await pageAgent.invoke_createIsolatedWorld({frameId, worldName: FREESTYLER_WORLD_NAME});
173
- const executionContext = runtimeModel?.executionContext(executionContextId);
174
- if (!executionContext) {
175
- throw new Error('Execution context is not found for executing code');
176
- }
177
-
178
- if (executionContext.debuggerModel.selectedCallFrame()) {
179
- return formatError('Cannot evaluate JavaScript because the execution is paused on a breakpoint.');
180
- }
181
-
182
- const remoteObject = await contextNode.resolveToObject(undefined, executionContextId);
183
- if (!remoteObject) {
184
- throw new Error('Cannot execute JavaScript because remote object cannot be resolved');
185
- }
186
-
187
- return await EvaluateAction.execute(functionDeclaration, [remoteObject], executionContext, {throwOnSideEffect});
188
- }
189
-
190
- const MAX_OBSERVATION_BYTE_LENGTH = 25_000;
191
- const OBSERVATION_TIMEOUT = 5_000;
192
-
193
146
  export const AI_ASSISTANCE_FILTER_REGEX = `\\.${AI_ASSISTANCE_CSS_CLASS_NAME}-.*&`;
194
147
 
195
148
  type CreateExtensionScopeFunction = (changes: ChangeManager) => {
@@ -310,6 +263,7 @@ export class StylingAgent extends AiAgent<SDK.DOMModel.DOMNode> {
310
263
  }
311
264
 
312
265
  #execJs: typeof executeJsCode;
266
+ #javascriptExecutor: JavascriptExecutor;
313
267
 
314
268
  #changes: ChangeManager;
315
269
  #createExtensionScope: CreateExtensionScopeFunction;
@@ -326,6 +280,14 @@ export class StylingAgent extends AiAgent<SDK.DOMModel.DOMNode> {
326
280
  opts.createExtensionScope ?? ((changes: ChangeManager) => {
327
281
  return new ExtensionScope(changes, this.sessionId, this.context?.getItem() ?? null, this.#currentTurnId);
328
282
  });
283
+ this.#javascriptExecutor = new JavascriptExecutor(
284
+ {
285
+ executionMode: this.executionMode,
286
+ getContextNode: () => this.#getSelectedNode(),
287
+ createExtensionScope: this.#createExtensionScope.bind(this),
288
+ changes: this.#changes,
289
+ },
290
+ this.#execJs);
329
291
 
330
292
  this.declareFunction<{
331
293
  elements: number[],
@@ -378,91 +340,7 @@ export class StylingAgent extends AiAgent<SDK.DOMModel.DOMNode> {
378
340
  },
379
341
  });
380
342
 
381
- this.declareFunction<{
382
- title: string,
383
- explanation: string,
384
- code: string,
385
- }>('executeJavaScript', {
386
- description:
387
- `This function allows you to run JavaScript code on the inspected page to access the element styles and page content.
388
- Call this function to gather additional information or modify the page state. Call this function enough times to investigate the user request.`,
389
- parameters: {
390
- type: Host.AidaClient.ParametersTypes.OBJECT,
391
- description: '',
392
- nullable: false,
393
- properties: {
394
- code: {
395
- type: Host.AidaClient.ParametersTypes.STRING,
396
- description:
397
- `JavaScript code snippet to run on the inspected page. Make sure the code is formatted for readability.
398
-
399
- # Instructions
400
-
401
- * To return data, define a top-level \`data\` variable and populate it with data you want to get. Only JSON-serializable objects can be assigned to \`data\`.
402
- * If you modify styles on an element, ALWAYS call the pre-defined global \`async setElementStyles(el: Element, styles: object)\` function. This function is an internal mechanism for you and should never be presented as a command/advice to the user.
403
- * **CRITICAL** Only get styles that might be relevant to the user request.
404
- * **CRITICAL** Never assume a selector for the elements unless you verified your knowledge.
405
- * **CRITICAL** Consider that \`data\` variable from the previous function calls are not available in a new function call.
406
-
407
- For example, the code to change element styles:
408
-
409
- \`\`\`
410
- await setElementStyles($0, {
411
- color: 'blue',
412
- });
413
- \`\`\`
414
-
415
- For example, the code to get overlapping elements:
416
-
417
- \`\`\`
418
- const data = {
419
- overlappingElements: Array.from(document.querySelectorAll('*'))
420
- .filter(el => {
421
- const rect = el.getBoundingClientRect();
422
- const popupRect = $0.getBoundingClientRect();
423
- return (
424
- el !== $0 &&
425
- rect.left < popupRect.right &&
426
- rect.right > popupRect.left &&
427
- rect.top < popupRect.bottom &&
428
- rect.bottom > popupRect.top
429
- );
430
- })
431
- .map(el => ({
432
- tagName: el.tagName,
433
- id: el.id,
434
- className: el.className,
435
- zIndex: window.getComputedStyle(el)['z-index']
436
- }))
437
- };
438
- \`\`\`
439
- `,
440
- },
441
- explanation: {
442
- type: Host.AidaClient.ParametersTypes.STRING,
443
- description: 'Explain why you want to run this code',
444
- },
445
- title: {
446
- type: Host.AidaClient.ParametersTypes.STRING,
447
- description: 'Provide a summary of what the code does. For example, "Checking related element styles".',
448
- },
449
- },
450
- required: ['code', 'explanation', 'title']
451
- },
452
- displayInfoFromArgs: params => {
453
- return {
454
- title: params.title,
455
- thought: params.explanation,
456
- action: params.code,
457
- };
458
- },
459
- handler: async (
460
- params,
461
- options,
462
- ) => {
463
- return await this.executeAction(params.code, options);
464
- },
465
- });
343
+ this.declareFunction('executeJavaScript', executeJavaScriptFunction(this.#javascriptExecutor));
466
344
 
467
345
  if (Annotations.AnnotationRepository.annotationsEnabled()) {
468
346
  this.declareFunction<{
@@ -528,68 +406,6 @@ const data = {
528
406
  });
529
407
  }
530
408
 
531
- async generateObservation(
532
- action: string,
533
- {
534
- throwOnSideEffect,
535
- }: {
536
- throwOnSideEffect: boolean,
537
- },
538
- ): Promise<{
539
- observation: string,
540
- sideEffect: boolean,
541
- canceled: boolean,
542
- }> {
543
- const functionDeclaration = `async function ($0) {
544
- try {
545
- ${action}
546
- ;
547
- return ((typeof data !== "undefined") ? data : undefined);
548
- } catch (error) {
549
- return error;
550
- }
551
- }`;
552
- try {
553
- const result = await Promise.race([
554
- this.#execJs(
555
- functionDeclaration,
556
- {
557
- throwOnSideEffect,
558
- contextNode: this.context?.getItem() || null,
559
- },
560
- ),
561
- new Promise<never>((_, reject) => {
562
- setTimeout(
563
- () => reject(new Error('Script execution exceeded the maximum allowed time.')), OBSERVATION_TIMEOUT);
564
- }),
565
- ]);
566
- const byteCount = Platform.StringUtilities.countWtf8Bytes(result);
567
- Host.userMetrics.freestylerEvalResponseSize(byteCount);
568
- if (byteCount > MAX_OBSERVATION_BYTE_LENGTH) {
569
- throw new Error('Output exceeded the maximum allowed length.');
570
- }
571
- return {
572
- observation: result,
573
- sideEffect: false,
574
- canceled: false,
575
- };
576
- } catch (error) {
577
- if (error instanceof SideEffectError) {
578
- return {
579
- observation: error.message,
580
- sideEffect: true,
581
- canceled: false,
582
- };
583
- }
584
-
585
- return {
586
- observation: `Error: ${error.message}`,
587
- sideEffect: false,
588
- canceled: false,
589
- };
590
- }
591
- }
592
-
593
409
  static async describeElement(element: SDK.DOMModel.DOMNode): Promise<string> {
594
410
  let output = `* Element's uid is ${element.backendNodeId()}.
595
411
  * Its selector is \`${element.simpleSelector()}\``;
@@ -742,74 +558,6 @@ const data = {
742
558
  };
743
559
  }
744
560
 
745
- async executeAction(action: string, options?: FunctionHandlerOptions): Promise<FunctionCallHandlerResult<unknown>> {
746
- debugLog(`Action to execute: ${action}`);
747
-
748
- if (options?.approved === false) {
749
- return {
750
- error: 'Error: User denied code execution with side effects.',
751
- };
752
- }
753
-
754
- if (this.executionMode === Root.Runtime.HostConfigFreestylerExecutionMode.NO_SCRIPTS) {
755
- return {
756
- error: 'Error: JavaScript execution is currently disabled.',
757
- };
758
- }
759
-
760
- const selectedNode = this.#getSelectedNode();
761
- if (!selectedNode) {
762
- return {error: 'Error: no selected node found.'};
763
- }
764
- const target = selectedNode.domModel().target();
765
- if (target.model(SDK.DebuggerModel.DebuggerModel)?.selectedCallFrame()) {
766
- return {
767
- error: 'Error: Cannot evaluate JavaScript because the execution is paused on a breakpoint.',
768
- };
769
- }
770
-
771
- const scope = this.#createExtensionScope(this.#changes);
772
- await scope.install();
773
- try {
774
- let throwOnSideEffect = true;
775
- if (options?.approved) {
776
- throwOnSideEffect = false;
777
- }
778
-
779
- const result = await this.generateObservation(action, {throwOnSideEffect});
780
- debugLog(`Action result: ${JSON.stringify(result)}`);
781
- if (result.sideEffect) {
782
- if (this.executionMode === Root.Runtime.HostConfigFreestylerExecutionMode.SIDE_EFFECT_FREE_SCRIPTS_ONLY) {
783
- return {
784
- error: 'Error: JavaScript execution that modifies the page is currently disabled.',
785
- };
786
- }
787
-
788
- if (options?.signal?.aborted) {
789
- return {
790
- error: 'Error: evaluation has been cancelled',
791
- };
792
- }
793
-
794
- return {
795
- requiresApproval: true,
796
- description: lockedString('This code may modify page content. Continue?'),
797
- };
798
- }
799
- if (result.canceled) {
800
- return {
801
- error: result.observation,
802
- };
803
- }
804
-
805
- return {
806
- result: result.observation,
807
- };
808
- } finally {
809
- await scope.uninstall();
810
- }
811
- }
812
-
813
561
  async addElementAnnotation(elementId: string, annotationMessage: string):
814
562
  Promise<FunctionCallHandlerResult<unknown>> {
815
563
  if (!Annotations.AnnotationRepository.annotationsEnabled()) {
@@ -13,8 +13,9 @@ import * as Root from '../../../core/root/root.js';
13
13
  import * as SDK from '../../../core/sdk/sdk.js';
14
14
  import type * as Protocol from '../../../generated/protocol.js';
15
15
  import type {
16
- AiWidget, ComputedStyleAiWidget, CoreVitalsAiWidget, DomTreeAiWidget, LcpBreakdownAiWidget, PerformanceTraceAiWidget,
17
- StylePropertiesAiWidget, TimelineRangeSummaryAiWidget} from '../../../models/ai_assistance/agents/AiAgent.js';
16
+ AiWidget, BottomUpTreeAiWidget, ComputedStyleAiWidget, CoreVitalsAiWidget, DomTreeAiWidget, LcpBreakdownAiWidget,
17
+ PerformanceTraceAiWidget, StylePropertiesAiWidget,
18
+ TimelineRangeSummaryAiWidget} from '../../../models/ai_assistance/agents/AiAgent.js';
18
19
  import * as AiAssistanceModel from '../../../models/ai_assistance/ai_assistance.js';
19
20
  import * as ComputedStyle from '../../../models/computed_style/computed_style.js';
20
21
  import * as Trace from '../../../models/trace/trace.js';
@@ -193,7 +194,15 @@ const UIStringsNotTranslate = {
193
194
  /**
194
195
  * @description Title for the performance summary widget.
195
196
  */
196
- performanceSummary: 'Performance summary'
197
+ performanceSummary: 'Performance summary',
198
+ /**
199
+ * @description The title of the button that allows exporting the conversation for agents.
200
+ */
201
+ exportForAgents: 'Copy for your coding agent',
202
+ /**
203
+ * @description Title for the bottom up thread activity widget.
204
+ */
205
+ bottomUpTree: 'Bottom-up thread activity',
197
206
  } as const;
198
207
 
199
208
  export interface Step {
@@ -260,6 +269,7 @@ export interface RatingViewInput {
260
269
  export interface ActionViewInput {
261
270
  onReportClick: () => void;
262
271
  onCopyResponseClick: () => void;
272
+ onExportClick?: () => void;
263
273
  showActions: boolean;
264
274
  }
265
275
 
@@ -293,11 +303,13 @@ export interface MessageInput {
293
303
  isLoading: boolean;
294
304
  isReadOnly: boolean;
295
305
  isLastMessage: boolean;
306
+ isFirstMessage: boolean;
296
307
  canShowFeedbackForm: boolean;
297
308
  markdownRenderer: MarkdownLitRenderer;
298
309
  onSuggestionClick: (suggestion: string) => void;
299
310
  onFeedbackSubmit: (rpcId: Host.AidaClient.RpcGlobalId, rate: Host.AidaClient.Rating, feedback?: string) => void;
300
311
  onCopyResponseClick: (message: ModelChatMessage) => void;
312
+ onExportClick?: () => void;
301
313
  walkthrough: {
302
314
  onOpen: (message: ModelChatMessage) => void,
303
315
  isExpanded: boolean,
@@ -309,22 +321,37 @@ export interface MessageInput {
309
321
  }
310
322
 
311
323
  export const DEFAULT_VIEW = (input: ChatMessageViewInput, output: ViewOutput, target: HTMLElement): void => {
324
+ const hasAiV2 = Boolean(Root.Runtime.hostConfig.devToolsAiAssistanceV2?.enabled);
312
325
  const message = input.message;
326
+
313
327
  if (message.entity === ChatMessageEntity.USER) {
314
328
  const imageInput = message.imageInput && 'inlineData' in message.imageInput ?
315
329
  renderImageChatMessage(message.imageInput.inlineData) :
316
330
  Lit.nothing;
331
+
332
+ const messageClasses = Lit.Directives.classMap({
333
+ 'chat-message': true,
334
+ query: true,
335
+ 'is-last-message': input.isLastMessage,
336
+ 'is-first-message': input.isFirstMessage,
337
+ 'ai-v2': hasAiV2,
338
+ });
339
+
340
+ const userQueryWrapperClasses = Lit.Directives.classMap({
341
+ // Don't need to style at all unless we are on the V2 flag.
342
+ // Once we ship this can be removed entirely.
343
+ 'user-query-wrapper': hasAiV2
344
+ });
317
345
  // clang-format off
318
346
  Lit.render(html`
319
347
  <style>${Input.textInputStyles}</style>
320
348
  <style>${chatMessageStyles}</style>
321
- <section
322
- class="chat-message query ${input.isLastMessage ? 'is-last-message' : ''}"
323
- jslog=${VisualLogging.section('question')}
324
- >
325
- ${imageInput}
326
- <div class="message-content">${renderTextAsMarkdown(message.text, input.markdownRenderer)}</div>
327
- </section>
349
+ <div class=${userQueryWrapperClasses}>
350
+ <section class=${messageClasses} jslog=${VisualLogging.section('question')}>
351
+ ${imageInput}
352
+ <div class="message-content">${renderTextAsMarkdown(message.text, input.markdownRenderer)}</div>
353
+ </section>
354
+ </div>
328
355
  `, target);
329
356
  // clang-format on
330
357
  return;
@@ -332,48 +359,53 @@ export const DEFAULT_VIEW = (input: ChatMessageViewInput, output: ViewOutput, ta
332
359
 
333
360
  const steps = message.parts.filter(part => part.type === 'step').map(part => part.step);
334
361
  const icon = AiAssistanceModel.AiUtils.getIconName();
335
- const aiAssistanceV2 = Root.Runtime.hostConfig.devToolsAiAssistanceV2?.enabled;
336
362
 
363
+ const messageClasses = Lit.Directives.classMap({
364
+ 'chat-message': true,
365
+ answer: true,
366
+ 'is-last-message': input.isLastMessage,
367
+ 'is-first-message': input.isFirstMessage,
368
+ 'ai-v2': hasAiV2,
369
+ });
337
370
  // clang-format off
338
371
  Lit.render(html`
339
372
  <style>${Input.textInputStyles}</style>
340
373
  <style>${chatMessageStyles}</style>
341
- <section
342
- class="chat-message answer ${input.isLastMessage ? 'is-last-message' : ''}"
343
- jslog=${VisualLogging.section('answer')}
344
- >
345
- ${aiAssistanceV2 ? Lit.nothing : html`
374
+ <section class=${messageClasses} jslog=${VisualLogging.section('answer')}>
375
+ ${hasAiV2 ? Lit.nothing : html`
346
376
  <div class="message-info">
347
377
  <devtools-icon name=${icon}></devtools-icon>
348
378
  <div class="message-name">
349
379
  <h2>${AiAssistanceModel.AiUtils.isGeminiBranding() ? lockedString(UIStringsNotTranslate.gemini) : lockedString(UIStringsNotTranslate.ai)}</h2>
350
380
  </div>
351
381
  </div>`}
352
- ${aiAssistanceV2 ? renderWalkthroughUI(input, steps) : Lit.nothing}
353
- ${Lit.Directives.repeat(
354
- message.parts,
355
- (_, index) => index,
356
- (part, index) => {
357
- const isLastPart = index === message.parts.length - 1;
358
- if (part.type === 'answer') {
359
- return html`<p>${renderTextAsMarkdown(part.text, input.markdownRenderer, { animate: !input.isReadOnly && input.isLoading && isLastPart && input.isLastMessage })}</p>`;
360
- }
361
- if (part.type === 'widget') {
362
- return html`${Lit.Directives.until(renderWidgets(part.widgets, {wrapperClass: 'main-widgets-wrapper'}))}`;
363
- }
364
- if (!aiAssistanceV2 && part.type === 'step') {
365
- return renderStep({
366
- step: part.step,
367
- isLoading: input.isLoading,
368
- markdownRenderer: input.markdownRenderer,
369
- isLast: isLastPart,
370
- });
371
- }
372
- return Lit.nothing;
373
- },
374
- )}
375
- ${renderError(message)}
376
- ${input.showActions ? renderActions(input, output) : Lit.nothing}
382
+ ${hasAiV2 ? renderWalkthroughUI(input, steps) : Lit.nothing}
383
+ <div class="answer-body-wrapper">
384
+ ${Lit.Directives.repeat(
385
+ message.parts,
386
+ (_, index) => index,
387
+ (part, index) => {
388
+ const isLastPart = index === message.parts.length - 1;
389
+ if (part.type === 'answer') {
390
+ return html`<p>${renderTextAsMarkdown(part.text, input.markdownRenderer, { animate: !input.isReadOnly && input.isLoading && isLastPart && input.isLastMessage })}</p>`;
391
+ }
392
+ if (part.type === 'widget') {
393
+ return html`${Lit.Directives.until(renderWidgets(part.widgets, {wrapperClass: 'main-widgets-wrapper'}))}`;
394
+ }
395
+ if (!hasAiV2 && part.type === 'step') {
396
+ return renderStep({
397
+ step: part.step,
398
+ isLoading: input.isLoading,
399
+ markdownRenderer: input.markdownRenderer,
400
+ isLast: isLastPart,
401
+ });
402
+ }
403
+ return Lit.nothing;
404
+ },
405
+ )}
406
+ ${renderError(message)}
407
+ ${input.showActions ? renderActions(input, output) : Lit.nothing}
408
+ </div>
377
409
  </section>
378
410
  `, target);
379
411
  // clang-format on
@@ -739,10 +771,9 @@ async function makeComputedStyleWidget(widgetData: ComputedStyleAiWidget): Promi
739
771
  };
740
772
  }
741
773
 
742
- async function makeCoreVitalsWidget(widgetData: CoreVitalsAiWidget): Promise<WidgetMakerResponse|null> {
774
+ async function makeCoreWebVitalsWidget(widgetData: CoreVitalsAiWidget): Promise<WidgetMakerResponse|null> {
743
775
  // clang-format off
744
- const renderedWidget = html`<devtools-widget
745
- class="core-vitals-widget" ${widget(TimelineComponents.CWVMetrics.CWVMetrics, {data: widgetData.data})}>
776
+ const renderedWidget = html`<devtools-widget class="core-vitals-widget" ${widget(TimelineComponents.CWVMetrics.CWVMetrics, {data: widgetData.data, skipBottomBorder: true})}>
746
777
  </devtools-widget>`;
747
778
  // clang-format on
748
779
 
@@ -802,6 +833,33 @@ async function makeLcpBreakdownWidget(widgetData: LcpBreakdownAiWidget): Promise
802
833
  };
803
834
  }
804
835
 
836
+ async function makeBottomUpTimelineTreeWidget(widgetData: BottomUpTreeAiWidget): Promise<WidgetMakerResponse|null> {
837
+ const bottomUpRootNode = AiAssistanceModel.AIQueries.AIQueries.mainThreadActivityBottomUp(
838
+ widgetData.data.bounds, widgetData.data.parsedTrace);
839
+ if (!bottomUpRootNode) {
840
+ return null;
841
+ }
842
+ const events = bottomUpRootNode.events;
843
+ const startTime = Trace.Helpers.Timing.microToMilli(widgetData.data.bounds.min);
844
+ const endTime = Trace.Helpers.Timing.microToMilli(widgetData.data.bounds.max);
845
+
846
+ const renderedWidget = html`<devtools-widget
847
+ class="bottom-up-timeline-tree-widget"
848
+ ${widget(Timeline.TimelineTreeView.BottomUpTimelineTreeView, {
849
+ selectedEvents: events,
850
+ parsedTrace: widgetData.data.parsedTrace,
851
+ startTime,
852
+ endTime,
853
+ compactMode: true,
854
+ })}></devtools-widget>`;
855
+
856
+ return {
857
+ renderedWidget,
858
+ revealable: new TimelineUtils.Helpers.RevealableBottomUpProfile(widgetData.data.bounds),
859
+ title: lockedString(UIStringsNotTranslate.bottomUpTree)
860
+ };
861
+ }
862
+
805
863
  function renderWidgetResponse(response: WidgetMakerResponse|null): Lit.LitTemplate {
806
864
  if (response === null) {
807
865
  return Lit.nothing;
@@ -911,6 +969,7 @@ async function makeDomTreeWidget(widgetData: DomTreeAiWidget): Promise<WidgetMak
911
969
  rootDOMNode: root,
912
970
  visibleWidth: 400,
913
971
  wrap: true,
972
+ maxRows: 10,
914
973
  })}></devtools-widget>
915
974
  `;
916
975
  // clang-format on
@@ -951,7 +1010,7 @@ async function renderWidgets(
951
1010
  response = await makeComputedStyleWidget(widgetData);
952
1011
  break;
953
1012
  case 'CORE_VITALS':
954
- response = await makeCoreVitalsWidget(widgetData);
1013
+ response = await makeCoreWebVitalsWidget(widgetData);
955
1014
  break;
956
1015
  case 'STYLE_PROPERTIES':
957
1016
  response = await makeStylePropertiesWidget(widgetData);
@@ -968,6 +1027,9 @@ async function renderWidgets(
968
1027
  case 'TIMELINE_RANGE_SUMMARY':
969
1028
  response = await makeTimelineRangeSummaryWidget(widgetData);
970
1029
  break;
1030
+ case 'BOTTOM_UP_TREE':
1031
+ response = await makeBottomUpTimelineTreeWidget(widgetData);
1032
+ break;
971
1033
  default:
972
1034
  Platform.assertNever(widgetData, 'Unknown AiWidget name');
973
1035
  }
@@ -1066,9 +1128,14 @@ function renderImageChatMessage(inlineData: Host.AidaClient.MediaBlob): Lit.LitT
1066
1128
  }
1067
1129
 
1068
1130
  function renderActions(input: ChatMessageViewInput, output: ViewOutput): Lit.LitTemplate {
1131
+ const aiAssistanceV2 = Root.Runtime.hostConfig.devToolsAiAssistanceV2?.enabled;
1132
+ const rowClasses = Lit.Directives.classMap({
1133
+ 'ai-assistance-feedback-row': true,
1134
+ 'not-v2': !aiAssistanceV2,
1135
+ });
1069
1136
  // clang-format off
1070
1137
  return html`
1071
- <div class="ai-assistance-feedback-row">
1138
+ <div class=${rowClasses}>
1072
1139
  <div class="action-buttons">
1073
1140
  ${input.showRateButtons ? html`
1074
1141
  <devtools-button
@@ -1111,7 +1178,8 @@ function renderActions(input: ChatMessageViewInput, output: ViewOutput): Lit.Lit
1111
1178
  }
1112
1179
  @click=${input.onReportClick}
1113
1180
  ></devtools-button>
1114
- <div class="vertical-separator"></div>
1181
+ ${aiAssistanceV2 ? Lit.nothing : html`
1182
+ <div class="vertical-separator"></div>
1115
1183
  <devtools-button
1116
1184
  .data=${{
1117
1185
  variant: Buttons.Button.Variant.ICON,
@@ -1122,6 +1190,18 @@ function renderActions(input: ChatMessageViewInput, output: ViewOutput): Lit.Lit
1122
1190
  } as Buttons.Button.ButtonData}
1123
1191
  aria-label=${lockedString(UIStringsNotTranslate.copyResponse)}
1124
1192
  @click=${input.onCopyResponseClick}></devtools-button>
1193
+ `}
1194
+ ${input.onExportClick && aiAssistanceV2 && input.isLastMessage ? html`
1195
+ <div class="vertical-separator"></div>
1196
+ <devtools-button
1197
+ class="export-for-agents-button"
1198
+ .jslogContext=${'ai-export-for-agents'}
1199
+ .variant=${Buttons.Button.Variant.OUTLINED}
1200
+ .iconName=${'copy'}
1201
+ @click=${input.onExportClick}
1202
+ >${lockedString(UIStringsNotTranslate.exportForAgents)}</devtools-button>
1203
+ <div class="vertical-separator"></div>
1204
+ ` : Lit.nothing}
1125
1205
  </div>
1126
1206
  ${input.suggestions ? html`<div class="suggestions-container">
1127
1207
  <div class="scroll-button-container left hidden" ${ref(element => { output.suggestionsLeftScrollButtonContainer = element; } )}>
@@ -1225,11 +1305,13 @@ export class ChatMessage extends UI.Widget.Widget {
1225
1305
  isReadOnly = false;
1226
1306
  canShowFeedbackForm = false;
1227
1307
  isLastMessage = false;
1308
+ isFirstMessage = false;
1228
1309
  markdownRenderer!: MarkdownLitRenderer;
1229
1310
  onSuggestionClick: (suggestion: string) => void = () => {};
1230
1311
  onFeedbackSubmit:
1231
1312
  (rpcId: Host.AidaClient.RpcGlobalId, rate: Host.AidaClient.Rating, feedback?: string) => void = () => {};
1232
1313
  onCopyResponseClick: (message: ModelChatMessage) => void = () => {};
1314
+ onExportClick: () => void = () => {};
1233
1315
  walkthrough: MessageInput['walkthrough'] = {
1234
1316
  onOpen: () => {},
1235
1317
  onToggle: () => {},
@@ -1272,6 +1354,7 @@ export class ChatMessage extends UI.Widget.Widget {
1272
1354
  canShowFeedbackForm: this.canShowFeedbackForm,
1273
1355
  markdownRenderer: this.markdownRenderer,
1274
1356
  isLastMessage: this.isLastMessage,
1357
+ isFirstMessage: this.isFirstMessage,
1275
1358
  onSuggestionClick: this.onSuggestionClick,
1276
1359
  onRatingClick: this.#handleRateClick.bind(this),
1277
1360
  onReportClick: () => UIHelpers.openInNewTab(REPORT_URL),
@@ -1280,6 +1363,7 @@ export class ChatMessage extends UI.Widget.Widget {
1280
1363
  this.onCopyResponseClick(this.message);
1281
1364
  }
1282
1365
  },
1366
+ onExportClick: this.onExportClick,
1283
1367
  scrollSuggestionsScrollContainer: this.#scrollSuggestionsScrollContainer.bind(this),
1284
1368
  onSuggestionsScrollOrResize: this.#handleSuggestionsScrollOrResize.bind(this),
1285
1369
  onSubmit: this.#handleSubmit.bind(this),
@@ -1436,7 +1520,7 @@ async function makeTimelineRangeSummaryWidget(widgetData: TimelineRangeSummaryAi
1436
1520
  widget(Timeline.ThirdPartyTreeView.ThirdPartyTreeViewWidget, {
1437
1521
  maxRows: 10,
1438
1522
  model: {
1439
- selectedEvents: thirdPartyTree.selectedEvents() ?? null,
1523
+ selectedEvents: thirdPartyTree.selectedEvents ?? null,
1440
1524
  parsedTrace,
1441
1525
  entityMapper: thirdPartyTree.entityMapper(),
1442
1526
  },