chrome-devtools-frontend 1.0.1596535 → 1.0.1597624

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 (101) hide show
  1. package/agents/prompts/ui-widgets.md +7 -8
  2. package/docs/ui_engineering.md +10 -11
  3. package/front_end/core/host/AidaClient.ts +4 -0
  4. package/front_end/core/host/InspectorFrontendHostAPI.ts +1 -0
  5. package/front_end/core/host/UserMetrics.ts +12 -0
  6. package/front_end/core/root/Runtime.ts +5 -0
  7. package/front_end/core/sdk/CPUThrottlingManager.ts +14 -13
  8. package/front_end/core/sdk/CSSMatchedStyles.ts +2 -0
  9. package/front_end/core/sdk/CSSPropertyParserMatchers.ts +28 -0
  10. package/front_end/core/sdk/PageResourceLoader.ts +22 -1
  11. package/front_end/devtools_compatibility.js +2 -1
  12. package/front_end/models/ai_assistance/AiConversation.ts +29 -8
  13. package/front_end/models/ai_assistance/ChangeManager.ts +16 -0
  14. package/front_end/models/ai_assistance/ExtensionScope.ts +11 -3
  15. package/front_end/models/ai_assistance/agents/AccessibilityAgent.ts +127 -0
  16. package/front_end/models/ai_assistance/agents/AiAgent.ts +26 -3
  17. package/front_end/models/ai_assistance/agents/ContextSelectionAgent.snapshot.txt +1 -1
  18. package/front_end/models/ai_assistance/agents/ContextSelectionAgent.ts +11 -8
  19. package/front_end/models/ai_assistance/agents/StylingAgent.snapshot.txt +24 -0
  20. package/front_end/models/ai_assistance/agents/StylingAgent.ts +323 -16
  21. package/front_end/models/ai_assistance/ai_assistance.ts +2 -0
  22. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.ts +27 -0
  23. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.snapshot.txt +21 -0
  24. package/front_end/models/greendev/Prototypes.ts +7 -1
  25. package/front_end/models/trace/Processor.ts +1 -0
  26. package/front_end/models/trace/handlers/PageLoadMetricsHandler.ts +33 -0
  27. package/front_end/models/trace/insights/CharacterSet.ts +172 -0
  28. package/front_end/models/trace/insights/Models.ts +1 -0
  29. package/front_end/models/trace/insights/types.ts +1 -0
  30. package/front_end/models/trace/types/TraceEvents.ts +17 -0
  31. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +51 -36
  32. package/front_end/panels/ai_assistance/PatchWidget.ts +6 -6
  33. package/front_end/panels/ai_assistance/components/ChatMessage.ts +93 -74
  34. package/front_end/panels/ai_assistance/components/ChatView.ts +6 -11
  35. package/front_end/panels/ai_assistance/components/MarkdownRendererWithCodeBlock.ts +18 -9
  36. package/front_end/panels/ai_assistance/components/StylingAgentMarkdownRenderer.ts +200 -0
  37. package/front_end/panels/ai_assistance/components/chatMessage.css +11 -8
  38. package/front_end/panels/application/AppManifestView.ts +3 -4
  39. package/front_end/panels/application/DeviceBoundSessionsView.ts +18 -22
  40. package/front_end/panels/application/FrameDetailsView.ts +9 -15
  41. package/front_end/panels/application/OriginTrialTreeView.ts +2 -3
  42. package/front_end/panels/application/ReportingApiView.ts +13 -17
  43. package/front_end/panels/application/ServiceWorkerCacheViews.ts +1 -1
  44. package/front_end/panels/application/components/BackForwardCacheView.ts +3 -3
  45. package/front_end/panels/application/preloading/components/UsedPreloadingView.ts +2 -3
  46. package/front_end/panels/browser_debugger/DOMBreakpointsSidebarPane.ts +3 -2
  47. package/front_end/panels/changes/ChangesView.ts +6 -4
  48. package/front_end/panels/common/ExtensionServer.ts +15 -0
  49. package/front_end/panels/console/ConsolePinPane.ts +3 -3
  50. package/front_end/panels/coverage/CoverageListView.ts +1 -1
  51. package/front_end/panels/css_overview/CSSOverviewPanel.ts +11 -15
  52. package/front_end/panels/developer_resources/DeveloperResourcesView.ts +3 -5
  53. package/front_end/panels/elements/ElementsTreeElement.ts +55 -47
  54. package/front_end/panels/elements/ElementsTreeOutline.ts +149 -28
  55. package/front_end/panels/elements/EventListenersWidget.ts +3 -2
  56. package/front_end/panels/elements/StandaloneStylesContainer.ts +21 -6
  57. package/front_end/panels/elements/StylePropertyTreeElement.ts +49 -4
  58. package/front_end/panels/layer_viewer/Layers3DView.ts +5 -4
  59. package/front_end/panels/lighthouse/LighthousePanel.ts +8 -0
  60. package/front_end/panels/linear_memory_inspector/components/LinearMemoryInspector.ts +5 -6
  61. package/front_end/panels/linear_memory_inspector/components/LinearMemoryValueInterpreter.ts +6 -11
  62. package/front_end/panels/network/RequestCookiesView.ts +3 -4
  63. package/front_end/panels/network/RequestInitiatorView.ts +7 -5
  64. package/front_end/panels/network/RequestResponseView.ts +10 -15
  65. package/front_end/panels/protocol_monitor/ProtocolMonitor.ts +3 -4
  66. package/front_end/panels/recorder/components/RecordingView.ts +31 -36
  67. package/front_end/panels/recorder/components/StepEditor.ts +6 -7
  68. package/front_end/panels/search/SearchView.ts +2 -3
  69. package/front_end/panels/settings/SettingsScreen.ts +3 -2
  70. package/front_end/panels/settings/WorkspaceSettingsTab.ts +2 -5
  71. package/front_end/panels/timeline/components/LiveMetricsView.ts +5 -8
  72. package/front_end/panels/timeline/components/insights/Cache.ts +8 -10
  73. package/front_end/panels/timeline/components/insights/CharacterSet.ts +38 -0
  74. package/front_end/panels/timeline/components/insights/DOMSize.ts +16 -20
  75. package/front_end/panels/timeline/components/insights/DocumentLatency.ts +2 -6
  76. package/front_end/panels/timeline/components/insights/DuplicatedJavaScript.ts +3 -4
  77. package/front_end/panels/timeline/components/insights/FontDisplay.ts +3 -4
  78. package/front_end/panels/timeline/components/insights/ForcedReflow.ts +5 -7
  79. package/front_end/panels/timeline/components/insights/INPBreakdown.ts +3 -4
  80. package/front_end/panels/timeline/components/insights/ImageDelivery.ts +3 -4
  81. package/front_end/panels/timeline/components/insights/ImageRef.ts +2 -4
  82. package/front_end/panels/timeline/components/insights/InsightRenderer.ts +2 -0
  83. package/front_end/panels/timeline/components/insights/LCPBreakdown.ts +5 -7
  84. package/front_end/panels/timeline/components/insights/LCPDiscovery.ts +2 -4
  85. package/front_end/panels/timeline/components/insights/LegacyJavaScript.ts +3 -4
  86. package/front_end/panels/timeline/components/insights/ModernHTTP.ts +3 -4
  87. package/front_end/panels/timeline/components/insights/NetworkDependencyTree.ts +7 -11
  88. package/front_end/panels/timeline/components/insights/NodeLink.ts +2 -4
  89. package/front_end/panels/timeline/components/insights/RenderBlocking.ts +3 -4
  90. package/front_end/panels/timeline/components/insights/SlowCSSSelector.ts +7 -10
  91. package/front_end/panels/timeline/components/insights/ThirdParties.ts +5 -7
  92. package/front_end/panels/timeline/components/insights/insights.ts +2 -0
  93. package/front_end/panels/web_audio/WebAudioView.ts +3 -4
  94. package/front_end/ui/components/settings/SettingCheckbox.ts +2 -0
  95. package/front_end/ui/legacy/UIUtils.ts +5 -5
  96. package/front_end/ui/legacy/Widget.ts +33 -2
  97. package/front_end/ui/legacy/components/data_grid/DataGridElement.ts +3 -3
  98. package/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts +8 -8
  99. package/front_end/ui/visual_logging/Debugging.ts +0 -32
  100. package/front_end/ui/visual_logging/KnownContextValues.ts +3 -0
  101. package/package.json +1 -1
@@ -13,7 +13,8 @@ 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} from '../../../models/ai_assistance/agents/AiAgent.js';
16
+ AiWidget, ComputedStyleAiWidget, CoreVitalsAiWidget, StylePropertiesAiWidget} from
17
+ '../../../models/ai_assistance/agents/AiAgent.js';
17
18
  import * as AiAssistanceModel from '../../../models/ai_assistance/ai_assistance.js';
18
19
  import * as ComputedStyle from '../../../models/computed_style/computed_style.js';
19
20
  import * as Marked from '../../../third_party/marked/marked.js';
@@ -34,6 +35,7 @@ import {walkthroughTitle, WalkthroughView} from './WalkthroughView.js';
34
35
 
35
36
  const {html, Directives: {ref, ifDefined}} = Lit;
36
37
  const lockedString = i18n.i18n.lockedString;
38
+ const {widget} = UI.Widget;
37
39
 
38
40
  const REPORT_URL = 'https://crbug.com/364805393' as Platform.DevToolsPath.UrlString;
39
41
  const SCROLL_ROUNDING_OFFSET = 1;
@@ -100,6 +102,10 @@ const UIStringsNotTranslate = {
100
102
  * @description The error message when the LLM gets stuck in a loop (max steps reached).
101
103
  */
102
104
  maxStepsError: 'Seems like I am stuck with the investigation. It would be better if you start over.',
105
+ /**
106
+ * @description The error message when the LLM selects context from a different origin.
107
+ */
108
+ crossOriginError: 'I have selected the new context but you will have to start a new chat.',
103
109
  /**
104
110
  * @description Displayed when the user stop the response
105
111
  */
@@ -120,10 +126,6 @@ const UIStringsNotTranslate = {
120
126
  * @description Gemini (do not translate)
121
127
  */
122
128
  gemini: 'Gemini',
123
- /**
124
- * @description The fallback text when we can't find the user full name
125
- */
126
- you: 'You',
127
129
  /**
128
130
  * @description The fallback text when a step has no title yet
129
131
  */
@@ -156,10 +158,6 @@ const UIStringsNotTranslate = {
156
158
  * @description Alt text for the image input (displayed in the chat messages) that has been sent to the model.
157
159
  */
158
160
  imageInputSentToTheModel: 'Image input sent to the model',
159
- /**
160
- * @description Alt text for the account avatar.
161
- */
162
- accountAvatar: 'Account avatar',
163
161
  /**
164
162
  * @description Title for the link which wraps the image input rendered in chat messages.
165
163
  */
@@ -212,7 +210,12 @@ export interface StepPart {
212
210
  step: Step;
213
211
  }
214
212
 
215
- export type ModelMessagePart = AnswerPart|StepPart;
213
+ export interface WidgetPart {
214
+ type: 'widget';
215
+ widgets: AiWidget[];
216
+ }
217
+
218
+ export type ModelMessagePart = AnswerPart|StepPart|WidgetPart;
216
219
 
217
220
  export interface UserChatMessage {
218
221
  entity: ChatMessageEntity.USER;
@@ -272,7 +275,6 @@ export interface MessageInput {
272
275
  isReadOnly: boolean;
273
276
  isLastMessage: boolean;
274
277
  canShowFeedbackForm: boolean;
275
- userInfo: Pick<Host.InspectorFrontendHostAPI.SyncInformation, 'accountImage'|'accountFullName'|'accountGivenName'>;
276
278
  markdownRenderer: MarkdownLitRenderer;
277
279
  onSuggestionClick: (suggestion: string) => void;
278
280
  onFeedbackSubmit: (rpcId: Host.AidaClient.RpcGlobalId, rate: Host.AidaClient.Rating, feedback?: string) => void;
@@ -289,14 +291,6 @@ export interface MessageInput {
289
291
  export const DEFAULT_VIEW = (input: ChatMessageViewInput, output: ViewOutput, target: HTMLElement): void => {
290
292
  const message = input.message;
291
293
  if (message.entity === ChatMessageEntity.USER) {
292
- const givenName = AiAssistanceModel.AiUtils.isGeminiBranding() ? input.userInfo.accountGivenName : '';
293
- const name = givenName || input.userInfo.accountFullName || lockedString(UIStringsNotTranslate.you);
294
- const image = input.userInfo.accountImage ?
295
- html`<img src="data:image/png;base64, ${input.userInfo.accountImage}" alt=${
296
- UIStringsNotTranslate.accountAvatar} />` :
297
- html`<devtools-icon
298
- name="profile"
299
- ></devtools-icon>`;
300
294
  const imageInput = message.imageInput && 'inlineData' in message.imageInput ?
301
295
  renderImageChatMessage(message.imageInput.inlineData) :
302
296
  Lit.nothing;
@@ -308,12 +302,6 @@ export const DEFAULT_VIEW = (input: ChatMessageViewInput, output: ViewOutput, ta
308
302
  class="chat-message query ${input.isLastMessage ? 'is-last-message' : ''}"
309
303
  jslog=${VisualLogging.section('question')}
310
304
  >
311
- <div class="message-info">
312
- ${image}
313
- <div class="message-name">
314
- <h2>${name}</h2>
315
- </div>
316
- </div>
317
305
  ${imageInput}
318
306
  <div class="message-content">${renderTextAsMarkdown(message.text, input.markdownRenderer)}</div>
319
307
  </section>
@@ -349,6 +337,9 @@ export const DEFAULT_VIEW = (input: ChatMessageViewInput, output: ViewOutput, ta
349
337
  if (part.type === 'answer') {
350
338
  return html`<p>${renderTextAsMarkdown(part.text, input.markdownRenderer, { animate: !input.isReadOnly && input.isLoading && isLastPart && input.isLastMessage })}</p>`;
351
339
  }
340
+ if (part.type === 'widget') {
341
+ return html`${Lit.Directives.until(renderWidgets(part.widgets, {wrapperClass: 'main-widgets-wrapper'}))}`;
342
+ }
352
343
  if (!aiAssistanceV2 && part.type === 'step') {
353
344
  return renderStep({
354
345
  step: part.step,
@@ -569,7 +560,7 @@ function renderWalkthroughUI(input: ChatMessageViewInput, steps: Step[]): Lit.Li
569
560
  // clang-format off
570
561
  const walkthroughInline = input.walkthrough.isInlined ? html`
571
562
  <div class="walkthrough-container">
572
- <devtools-widget .widgetConfig=${UI.Widget.widgetConfig(WalkthroughView, {
563
+ ${widget(WalkthroughView, {
573
564
  message: input.message as ModelChatMessage,
574
565
  isLoading: input.isLoading && input.isLastMessage,
575
566
  markdownRenderer: input.markdownRenderer,
@@ -577,7 +568,7 @@ function renderWalkthroughUI(input: ChatMessageViewInput, steps: Step[]): Lit.Li
577
568
  isExpanded,
578
569
  onToggle: input.walkthrough.onToggle,
579
570
  onOpen: input.walkthrough.onOpen,
580
- })}></devtools-widget>
571
+ })}
581
572
  </div>
582
573
  ` : Lit.nothing;
583
574
 
@@ -624,7 +615,6 @@ export function renderStep({step, isLoading, markdownRenderer, isLast}: {
624
615
  markdownRenderer: MarkdownLitRenderer,
625
616
  isLast: boolean,
626
617
  }): Lit.LitTemplate {
627
- const shouldRenderWidgets = Boolean(step.widgets?.length && Root.Runtime.hostConfig.devToolsAiAssistanceV2?.enabled);
628
618
  const stepClasses = Lit.Directives.classMap({
629
619
  step: true,
630
620
  empty: !step.thought && !step.code && !step.contextDetails && !step.requestApproval,
@@ -648,11 +638,8 @@ export function renderStep({step, isLoading, markdownRenderer, isLast}: {
648
638
  </summary>
649
639
  ${renderStepDetails({step, markdownRenderer, isLast})}
650
640
  </details>
651
- ${shouldRenderWidgets ? html`
652
- <div class="step-widgets-wrapper">
653
- ${Lit.Directives.until(renderStepWidgets(step))}
654
- </div>` : Lit.nothing
655
- }`;
641
+ ${Lit.Directives.until(renderWidgets(step.widgets, {wrapperClass: 'step-widgets-wrapper'}))}
642
+ `;
656
643
  // clang-format on
657
644
  }
658
645
 
@@ -661,25 +648,31 @@ interface WidgetMakerResponse {
661
648
  revealable: unknown;
662
649
  }
663
650
 
664
- const computedStyleNodeCache = new Map<Protocol.DOM.BackendNodeId, SDK.DOMModel.DOMNode>();
651
+ const nodeCache = new Map<Protocol.DOM.BackendNodeId, SDK.DOMModel.DOMNode>();
652
+
653
+ async function resolveNode(backendNodeId: Protocol.DOM.BackendNodeId): Promise<SDK.DOMModel.DOMNode|null> {
654
+ const cachedNode = nodeCache.get(backendNodeId);
655
+ if (cachedNode) {
656
+ return cachedNode;
657
+ }
658
+
659
+ const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
660
+ if (!target) {
661
+ return null;
662
+ }
663
+
664
+ const node = new SDK.DOMModel.DeferredDOMNode(target, backendNodeId);
665
+ const resolved = await node.resolvePromise();
666
+ if (resolved) {
667
+ nodeCache.set(backendNodeId, resolved);
668
+ }
669
+ return resolved;
670
+ }
665
671
 
666
672
  async function makeComputedStyleWidget(widgetData: ComputedStyleAiWidget): Promise<WidgetMakerResponse|null> {
667
- let domNodeForId = computedStyleNodeCache.get(widgetData.data.backendNodeId);
673
+ const domNodeForId = await resolveNode(widgetData.data.backendNodeId);
668
674
  if (!domNodeForId) {
669
- const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
670
- if (!target) {
671
- return null;
672
- }
673
- const node = new SDK.DOMModel.DeferredDOMNode(
674
- target,
675
- widgetData.data.backendNodeId,
676
- );
677
- const resolved = await node.resolvePromise();
678
- if (!resolved) {
679
- return null;
680
- }
681
- domNodeForId = resolved;
682
- computedStyleNodeCache.set(widgetData.data.backendNodeId, resolved);
675
+ return null;
683
676
  }
684
677
  const styles = new ComputedStyle.ComputedStyleModel.ComputedStyle(domNodeForId, widgetData.data.computedStyles);
685
678
 
@@ -712,6 +705,27 @@ async function makeCoreVitalsWidget(widgetData: CoreVitalsAiWidget): Promise<Wid
712
705
  };
713
706
  }
714
707
 
708
+ async function makeStylePropertiesWidget(widgetData: StylePropertiesAiWidget): Promise<WidgetMakerResponse|null> {
709
+ const domNodeForId = await resolveNode(widgetData.data.backendNodeId);
710
+ if (!domNodeForId) {
711
+ return null;
712
+ }
713
+
714
+ const widgetConfig = UI.Widget.widgetConfig(Elements.StandaloneStylesContainer.StandaloneStylesContainer, {
715
+ domNode: domNodeForId,
716
+ filter: widgetData.data.selector ? new RegExp(widgetData.data.selector) : null,
717
+ });
718
+
719
+ // clang-format off
720
+ const widget = html`<devtools-widget
721
+ class="styling-preview-widget"
722
+ .widgetConfig=${widgetConfig}
723
+ ></devtools-widget>`;
724
+ // clang-format on
725
+
726
+ return {renderedWidget: widget, revealable: domNodeForId};
727
+ }
728
+
715
729
  function renderWidgetResponse(response: WidgetMakerResponse|null): Lit.LitTemplate {
716
730
  if (response === null) {
717
731
  return Lit.nothing;
@@ -743,38 +757,43 @@ function renderWidgetResponse(response: WidgetMakerResponse|null): Lit.LitTempla
743
757
  }
744
758
 
745
759
  /**
746
- * Renders AI-defined UI widgets within a step.
747
- * When a ModelChatMessage contains a WidgetPart, the ChatMessage component
748
- * iterates through the `step.widgets` array. For each widget, it determines
749
- * the appropriate rendering logic based on the `widgetData.name`.
760
+ * Renders AI-defined UI widgets.
761
+ * When a ModelChatMessage contains a WidgetPart, or a Step has widgets,
762
+ * the ChatMessage component iterates through the \`widgets\` array.
763
+ * For each widget, it determines the appropriate rendering logic based on
764
+ * the \`widgetData.name\`.
750
765
  *
751
- * Currently, only 'COMPUTED_STYLES' and 'CORE_VITALS' widgets are supported. For these, the
752
- * `makeComputedStyleWidget` and `makeCoreVitalsWidget` functions are called to construct the necessary
753
- * data and configuration for the `Elements.ComputedStyleWidget.ComputedStyleWidget`
754
- * and `TimelineComponents.CWVMetrics.CWVMetrics`
755
- * components. The widget is then rendered using the `<devtools-widget>`
756
- * custom element, which dynamically instantiates and displays the specified
757
- * UI.Widget subclass with the provided configuration.
766
+ * Currently, 'COMPUTED_STYLES', 'CORE_VITALS' and 'STYLE_PROPERTIES' widgets are supported.
767
+ * For these, the corresponding \`make...Widget\` functions are called to construct the necessary
768
+ * data and configuration for the UI components. The widget is then rendered using the
769
+ * \`<devtools-widget>\` custom element, which dynamically instantiates and displays the
770
+ * specified UI.Widget subclass with the provided configuration.
758
771
  *
759
772
  * This allows for a flexible and extensible system where new widget types
760
773
  * can be added to the AI responses and rendered in DevTools by adding
761
- * corresponding `make...Widget` functions and handling them here.
774
+ * corresponding \`make...Widget\` functions and handling them here.
762
775
  */
763
- async function renderStepWidgets(step: Step): Promise<Lit.LitTemplate> {
764
- if (!step.widgets || step.widgets.length === 0) {
776
+ async function renderWidgets(
777
+ widgets: AiWidget[]|undefined, options: {wrapperClass?: string} = {}): Promise<Lit.LitTemplate> {
778
+ if (!Root.Runtime.hostConfig.devToolsAiAssistanceV2?.enabled || !widgets || widgets.length === 0) {
765
779
  return Lit.nothing;
766
780
  }
767
- const ui = await Promise.all(step.widgets.map(async widgetData => {
781
+ const ui = await Promise.all(widgets.map(async widgetData => {
782
+ let response: WidgetMakerResponse|null = null;
768
783
  if (widgetData.name === 'COMPUTED_STYLES') {
769
- const response = await makeComputedStyleWidget(widgetData);
770
- return renderWidgetResponse(response);
784
+ response = await makeComputedStyleWidget(widgetData);
785
+ } else if (widgetData.name === 'CORE_VITALS') {
786
+ response = await makeCoreVitalsWidget(widgetData);
787
+ } else if (widgetData.name === 'STYLE_PROPERTIES') {
788
+ response = await makeStylePropertiesWidget(widgetData);
771
789
  }
772
- if (widgetData.name === 'CORE_VITALS') {
773
- const response = await makeCoreVitalsWidget(widgetData);
774
- return renderWidgetResponse(response);
775
- }
776
- return Lit.nothing;
790
+ return renderWidgetResponse(response);
777
791
  }));
792
+
793
+ if (options.wrapperClass) {
794
+ return html`<div class=${options.wrapperClass}>${ui}</div>`;
795
+ }
796
+
778
797
  return html`${ui}`;
779
798
  }
780
799
 
@@ -829,6 +848,9 @@ function renderError(message: ModelChatMessage): Lit.LitTemplate {
829
848
  case AiAssistanceModel.AiAgent.ErrorType.MAX_STEPS:
830
849
  errorMessage = UIStringsNotTranslate.maxStepsError;
831
850
  break;
851
+ case AiAssistanceModel.AiAgent.ErrorType.CROSS_ORIGIN:
852
+ errorMessage = UIStringsNotTranslate.crossOriginError;
853
+ break;
832
854
  case AiAssistanceModel.AiAgent.ErrorType.ABORT:
833
855
  return html`<p class="aborted" jslog=${VisualLogging.section('aborted')}>${
834
856
  lockedString(UIStringsNotTranslate.stoppedResponse)}</p>`;
@@ -1019,8 +1041,6 @@ export class ChatMessage extends UI.Widget.Widget {
1019
1041
  isReadOnly = false;
1020
1042
  canShowFeedbackForm = false;
1021
1043
  isLastMessage = false;
1022
- userInfo:
1023
- Pick<Host.InspectorFrontendHostAPI.SyncInformation, 'accountImage'|'accountFullName'|'accountGivenName'> = {};
1024
1044
  markdownRenderer!: MarkdownLitRenderer;
1025
1045
  onSuggestionClick: (suggestion: string) => void = () => {};
1026
1046
  onFeedbackSubmit:
@@ -1067,7 +1087,6 @@ export class ChatMessage extends UI.Widget.Widget {
1067
1087
  isLoading: this.isLoading,
1068
1088
  isReadOnly: this.isReadOnly,
1069
1089
  canShowFeedbackForm: this.canShowFeedbackForm,
1070
- userInfo: this.userInfo,
1071
1090
  markdownRenderer: this.markdownRenderer,
1072
1091
  isLastMessage: this.isLastMessage,
1073
1092
  onSuggestionClick: this.onSuggestionClick,
@@ -25,6 +25,7 @@ const {
25
25
  repeat,
26
26
  classMap,
27
27
  } = Directives;
28
+ const {widget} = UI.Widget;
28
29
 
29
30
  /*
30
31
  * Strings that don't need to be translated at this time.
@@ -67,9 +68,8 @@ export interface Props {
67
68
  messages: Message[];
68
69
  context: AiAssistanceModel.AiAgent.ConversationContext<unknown>|null;
69
70
  isContextSelected: boolean;
70
- isLoading: boolean;
71
71
  canShowFeedbackForm: boolean;
72
- userInfo: Pick<Host.InspectorFrontendHostAPI.SyncInformation, 'accountImage'|'accountGivenName'>;
72
+ isLoading: boolean;
73
73
  conversationType: AiAssistanceModel.AiHistoryStorage.ConversationType;
74
74
  isReadOnly: boolean;
75
75
  blockedByCrossOrigin: boolean;
@@ -91,7 +91,6 @@ export interface Props {
91
91
  }
92
92
 
93
93
  interface ChatWidgetInput extends Props {
94
- accountGivenName: string;
95
94
  handleScroll: (ev: Event) => void;
96
95
  handleSuggestionClick: (title: string) => void;
97
96
  handleMessageContainerRef: (el: Element|undefined) => void;
@@ -116,12 +115,11 @@ const DEFAULT_VIEW: View = (input, output, target) => {
116
115
  ${input.messages.length > 0 ? html`
117
116
  <div class="messages-container" ${ref(input.handleMessageContainerRef)}>
118
117
  ${repeat(input.messages, message =>
119
- html`<devtools-widget .widgetConfig=${UI.Widget.widgetConfig(ChatMessage, {
118
+ widget(ChatMessage, {
120
119
  message,
121
120
  isLoading: input.isLoading && input.messages.at(-1) === message,
122
121
  isReadOnly: input.isReadOnly,
123
122
  canShowFeedbackForm: input.canShowFeedbackForm,
124
- userInfo: input.userInfo,
125
123
  markdownRenderer: input.markdownRenderer,
126
124
  isLastMessage: input.messages.at(-1) === message,
127
125
  onSuggestionClick: input.handleSuggestionClick,
@@ -130,14 +128,12 @@ const DEFAULT_VIEW: View = (input, output, target) => {
130
128
  walkthrough: {
131
129
  ...input.walkthrough,
132
130
  }
133
- })}></devtools-widget>`
131
+ })
134
132
  )}
135
- ${input.isLoading ? nothing : html`<devtools-widget
136
- .widgetConfig=${UI.Widget.widgetConfig(PatchWidget, {
133
+ ${input.isLoading ? nothing : widget(PatchWidget, {
137
134
  changeSummary: input.changeSummary ?? '',
138
135
  changeManager: input.changeManager,
139
136
  })}
140
- ></devtools-widget>`}
141
137
  </div>
142
138
  ` : html`
143
139
  <div class="empty-state-container">
@@ -149,7 +145,7 @@ const DEFAULT_VIEW: View = (input, output, target) => {
149
145
  </div>
150
146
  ${AiAssistanceModel.AiUtils.isGeminiBranding() ?
151
147
  html`
152
- <h1 class='greeting'>Hello${input.accountGivenName ? `, ${input.accountGivenName}` : ''}</h1>
148
+ <h1 class='greeting'>Hello</h1>
153
149
  <p class='cta'>${lockedString(UIStringsNotTranslate.emptyStateTextGemini)}</p>
154
150
  ` : html`<h1>${lockedString(UIStringsNotTranslate.emptyStateText)}</h1>`
155
151
  }
@@ -354,7 +350,6 @@ export class ChatView extends HTMLElement {
354
350
  this.#view(
355
351
  {
356
352
  ...this.#props,
357
- accountGivenName: this.#props.userInfo.accountGivenName ?? '',
358
353
  handleScroll: this.#handleScroll,
359
354
  handleSuggestionClick: this.#handleSuggestionClick,
360
355
  handleMessageContainerRef: this.#handleMessageContainerRef,
@@ -33,16 +33,21 @@ export class MarkdownRendererWithCodeBlock extends MarkdownView.MarkdownView.Mar
33
33
  }}>${Platform.StringUtilities.trimEndWithMaxLength(label, 100)}</devtools-link>`;
34
34
  }
35
35
 
36
- #renderLink(href: string): Lit.LitTemplate|null {
36
+ #renderLink(
37
+ href: string,
38
+ fallbackText: string,
39
+ ): Lit.LitTemplate|null {
37
40
  if (href.startsWith('#req-')) {
38
- const request =
39
- Logs.NetworkLog.NetworkLog.instance().requests().find(req => req.requestId() === href.substring(5));
41
+ const request = Logs.NetworkLog.NetworkLog.instance().requests().find(
42
+ req => req.requestId() === href.substring(5),
43
+ );
40
44
 
41
45
  if (request) {
42
46
  return this.#revealableLink(request, request.url());
43
47
  }
44
-
45
- } else if (href.startsWith('#file-')) {
48
+ return html`${fallbackText}`;
49
+ }
50
+ if (href.startsWith('#file-')) {
46
51
  const file = AiAssistanceModel.ContextSelectionAgent.ContextSelectionAgent.getUISourceCodes().find(
47
52
  file => AiAssistanceModel.ContextSelectionAgent.ContextSelectionAgent.uiSourceCodeId.get(file) ===
48
53
  Number(href.substring(6)));
@@ -50,13 +55,14 @@ export class MarkdownRendererWithCodeBlock extends MarkdownView.MarkdownView.Mar
50
55
  if (file) {
51
56
  return this.#revealableLink(file, file.name());
52
57
  }
58
+ return html`${fallbackText}`;
53
59
  }
54
60
  return null;
55
61
  }
56
62
 
57
63
  override templateForToken(token: Marked.Marked.MarkedToken): Lit.LitTemplate|null {
58
64
  if (token.type === 'link') {
59
- const link = this.#renderLink(token.href);
65
+ const link = this.#renderLink(token.href, token.text);
60
66
  if (link) {
61
67
  return link;
62
68
  }
@@ -73,9 +79,12 @@ export class MarkdownRendererWithCodeBlock extends MarkdownView.MarkdownView.Mar
73
79
  if (token.type === 'codespan') {
74
80
  // LLM likes outputting the link inside a codespan block.
75
81
  // Remove the codespan and render the link directly
76
- const matches = token.text.match(/^\[.*\]\((.+)\)$/);
77
- if (matches?.[1]) {
78
- const link = this.#renderLink(matches[1]);
82
+ const matches = token.text.match(/^\[(.*)\]\((.+)\)$/);
83
+ if (matches?.[2]) {
84
+ const link = this.#renderLink(
85
+ matches[2],
86
+ matches[1],
87
+ );
79
88
  if (link) {
80
89
  return link;
81
90
  }
@@ -0,0 +1,200 @@
1
+ // Copyright 2026 The Chromium Authors
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+
5
+ import * as SDK from '../../../core/sdk/sdk.js';
6
+ import type * as Protocol from '../../../generated/protocol.js';
7
+ import * as Marked from '../../../third_party/marked/marked.js';
8
+ import type * as MarkdownView from '../../../ui/components/markdown_view/MarkdownView.js';
9
+ import * as Lit from '../../../ui/lit/lit.js';
10
+ import * as PanelsCommon from '../../common/common.js';
11
+
12
+ import {MarkdownRendererWithCodeBlock} from './MarkdownRendererWithCodeBlock.js';
13
+
14
+ const {html} = Lit.StaticHtml;
15
+ const {ref, createRef} = Lit.Directives;
16
+
17
+ export class StylingAgentMarkdownRenderer extends MarkdownRendererWithCodeBlock {
18
+ constructor(
19
+ private mainFrameId = '',
20
+ ) {
21
+ super();
22
+ }
23
+
24
+ #renderTableFromJson(data: Array<Record<string, string>>): Lit.LitTemplate|null {
25
+ if (!Array.isArray(data) || data.length === 0 || typeof data[0] !== 'object' || data[0] === null) {
26
+ return null;
27
+ }
28
+
29
+ const headers = Object.keys(data[0]);
30
+ const requiredKeys = ['Problem', 'Element', 'NodeId', 'Details'];
31
+ if (!requiredKeys.every(key => headers.includes(key))) {
32
+ return null; // Not the expected JSON structure
33
+ }
34
+
35
+ const problemIndex = headers.indexOf('Problem');
36
+ if (problemIndex > -1) {
37
+ const problemHeader = headers.splice(problemIndex, 1);
38
+ headers.unshift(...problemHeader);
39
+ }
40
+
41
+ return html`
42
+ <table style="width: 100%;">
43
+ <thead>
44
+ <tr>
45
+ ${headers.map(header => html`<th style="text-align: left;">${header === 'NodeId' ? '' : header}</th>`)}
46
+ </tr>
47
+ </thead>
48
+ <tbody>
49
+ ${data.flatMap(row => {
50
+ return html`
51
+ <tr>
52
+ ${headers.map(header => {
53
+ if (header === 'NodeId') {
54
+ return html`<td>${this.#renderLinkifiedText(row[header])}</td>`;
55
+ }
56
+ if (header === 'Details') {
57
+ // eslint-disable-next-line @devtools/no-a-tags-in-lit
58
+ return html`<td><a href="#" @click=${this.#toggleDetailsRow}>Details</a></td>`;
59
+ }
60
+ return html`<td>${row[header]}</td>`;
61
+ })}
62
+ </tr>
63
+ <tr class="details-row" style="display: none;">
64
+ <td colspan=${headers.length} style="background-color: #f0f0f0; padding: 1em;">
65
+ <devtools-markdown-view .data=${{
66
+ tokens: Marked.Marked.lexer(row['Details']),
67
+ renderer: new StylingAgentMarkdownRenderer(this.mainFrameId),
68
+ } as MarkdownView.MarkdownViewData}></devtools-markdown-view>
69
+ </td>
70
+ </tr>
71
+ `;
72
+ })}
73
+ </tbody>
74
+ </table>
75
+ <br><div>To investigate these problems, please click one of the provided links (above), to set as context, and ask me further questions about the problem.</div>
76
+ `;
77
+ }
78
+
79
+ override templateForToken(token: Marked.Marked.MarkedToken): Lit.LitTemplate|null {
80
+ if (token.type === 'code') {
81
+ try {
82
+ const data = JSON.parse(token.text);
83
+ const table = this.#renderTableFromJson(data);
84
+ if (table) {
85
+ return table;
86
+ }
87
+ } catch {
88
+ // Not a JSON object, fallback to default rendering.
89
+ }
90
+ }
91
+
92
+ if (token.type === 'link' && token.href.startsWith('#')) {
93
+ let nodeId = undefined;
94
+ if (token.href.startsWith('#node-')) {
95
+ nodeId = Number(token.href.replace('#node-', '')) as Protocol.DOM.BackendNodeId;
96
+ } else if (token.href.startsWith('#')) {
97
+ // So often does it ignore requests to prepend nodes with node-, frustratingly.
98
+ nodeId = Number(token.href.replace('#', '')) as Protocol.DOM.BackendNodeId;
99
+ }
100
+
101
+ if (nodeId) {
102
+ const templateRef = createRef();
103
+ void this.#linkifyNode(nodeId, token.text).then(node => {
104
+ if (!templateRef.value || !node) {
105
+ return;
106
+ }
107
+
108
+ templateRef.value.textContent = '';
109
+ templateRef.value.append(node);
110
+ });
111
+ return html`<span ${ref(templateRef)}>${token.text}</span>`;
112
+ }
113
+ }
114
+
115
+ return super.templateForToken(token);
116
+ }
117
+
118
+ #toggleDetailsRow(e: Event): void {
119
+ e.preventDefault();
120
+ const link = e.target as HTMLAnchorElement;
121
+ const currentRow = link.closest('tr');
122
+ if (!currentRow) {
123
+ return;
124
+ }
125
+ const detailsRow = currentRow.nextElementSibling as HTMLTableRowElement | null;
126
+ if (detailsRow?.classList.contains('details-row')) {
127
+ if (detailsRow.style.display === 'none') {
128
+ detailsRow.style.display = 'table-row';
129
+ link.textContent = 'Hide';
130
+ } else {
131
+ detailsRow.style.display = 'none';
132
+ link.textContent = 'Details';
133
+ }
134
+ }
135
+ }
136
+
137
+ #renderLinkifiedText(text: string): Lit.LitTemplate {
138
+ if (text.indexOf(',') === -1) {
139
+ const nodeId = Number(text) as Protocol.DOM.BackendNodeId;
140
+ if (isNaN(nodeId)) {
141
+ // Not a number, return as is.
142
+ return html`${text}`;
143
+ }
144
+ return this.#renderSingleLink(nodeId);
145
+ }
146
+
147
+ // Check for comma separated list.
148
+ const nodeIdsStr = text.split(',').map(s => s.trim()).filter(Boolean);
149
+ return html`${nodeIdsStr.map(idStr => {
150
+ const nodeId = Number(idStr) as Protocol.DOM.BackendNodeId;
151
+ if (isNaN(nodeId)) {
152
+ return html`<div>${idStr}</div>`;
153
+ }
154
+ return html`<div>${this.#renderSingleLink(nodeId)}</div>`;
155
+ })}`;
156
+ }
157
+
158
+ #renderSingleLink(nodeId: Protocol.DOM.BackendNodeId): Lit.LitTemplate {
159
+ const label = `link`;
160
+ const templateRef = createRef();
161
+ void this.#linkifyNode(nodeId, label).then(node => {
162
+ if (!templateRef.value) {
163
+ return;
164
+ }
165
+ templateRef.value.textContent = '';
166
+ if (node) {
167
+ templateRef.value.append(node);
168
+ } else {
169
+ // Fallback to plain text if linkification fails
170
+ templateRef.value.append(document.createTextNode(label));
171
+ }
172
+ });
173
+ // Placeholder for async link
174
+ return html`<span ${ref(templateRef)}>${label}</span>`;
175
+ }
176
+
177
+ async #linkifyNode(backendNodeId: Protocol.DOM.BackendNodeId, label: string): Promise<Node|undefined> {
178
+ if (backendNodeId === undefined) {
179
+ return;
180
+ }
181
+
182
+ const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
183
+ const domModel = target?.model(SDK.DOMModel.DOMModel);
184
+ if (!domModel) {
185
+ return undefined;
186
+ }
187
+ const domNodesMap = await domModel.pushNodesByBackendIdsToFrontend(new Set([backendNodeId]));
188
+ const node = domNodesMap?.get(backendNodeId);
189
+ if (!node) {
190
+ return;
191
+ }
192
+
193
+ if (node.frameId() !== this.mainFrameId) {
194
+ return;
195
+ }
196
+
197
+ const linkedNode = PanelsCommon.DOMLinkifier.Linkifier.instance().linkify(node, {textContent: label});
198
+ return linkedNode;
199
+ }
200
+ }
@@ -143,14 +143,6 @@
143
143
  gap: var(--sys-size-4);
144
144
  font: var(--sys-typescale-body4-bold);
145
145
 
146
- img {
147
- border: 0;
148
- border-radius: var(--sys-shape-corner-full);
149
- display: block;
150
- height: var(--sys-size-9);
151
- width: var(--sys-size-9);
152
- }
153
-
154
146
  h2 {
155
147
  font: var(--sys-typescale-body4-bold);
156
148
  }
@@ -362,6 +354,17 @@
362
354
  width: fit-content;
363
355
  }
364
356
 
357
+ .styling-preview-widget {
358
+ width: 100%;
359
+ min-height: 100px;
360
+ }
361
+
362
+ .main-widgets-wrapper {
363
+ display: flex;
364
+ flex-direction: column;
365
+ gap: var(--sys-size-5);
366
+ }
367
+
365
368
  .step-widgets-wrapper {
366
369
  width: fit-content;
367
370
  display: flex;