chrome-devtools-frontend 1.0.1621064 → 1.0.1622369

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 (41) hide show
  1. package/.agents/skills/foundation-test-migration/SKILL.md +171 -0
  2. package/.agents/skills/verification/SKILL.md +2 -11
  3. package/front_end/core/common/Base64.ts +12 -2
  4. package/front_end/core/i18n/i18nImpl.ts +8 -4
  5. package/front_end/core/root/Runtime.ts +28 -9
  6. package/front_end/entrypoints/device_mode_emulation_frame/device_mode_emulation_frame.ts +1 -1
  7. package/front_end/entrypoints/shell/shell.ts +1 -1
  8. package/front_end/generated/InspectorBackendCommands.ts +1 -1
  9. package/front_end/generated/protocol.ts +1 -1
  10. package/front_end/models/ai_assistance/agents/AiAgent.ts +1 -1
  11. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +32 -20
  12. package/front_end/models/bindings/DebuggerWorkspaceBinding.ts +5 -1
  13. package/front_end/models/bindings/SymbolizedError.ts +18 -1
  14. package/front_end/models/issues_manager/GenericIssue.ts +50 -1
  15. package/front_end/models/issues_manager/descriptions/genericFormModelContextMissingToolDescription.md +5 -0
  16. package/front_end/models/issues_manager/descriptions/genericFormModelContextMissingToolName.md +5 -0
  17. package/front_end/models/issues_manager/descriptions/genericFormModelContextParameterMissingName.md +5 -0
  18. package/front_end/models/issues_manager/descriptions/genericFormModelContextParameterMissingTitleAndDescription.md +5 -0
  19. package/front_end/models/issues_manager/descriptions/genericFormModelContextRequiredParameterMissingName.md +5 -0
  20. package/front_end/models/javascript_metadata/NativeFunctions.js +8 -0
  21. package/front_end/models/trace/insights/Common.ts +4 -0
  22. package/front_end/models/trace/insights/types.ts +1 -1
  23. package/front_end/models/web_mcp/WebMCPModel.ts +11 -1
  24. package/front_end/panels/ai_assistance/components/ChatMessage.ts +94 -23
  25. package/front_end/panels/application/WebMCPView.ts +16 -7
  26. package/front_end/panels/console/SymbolizedErrorWidget.ts +69 -0
  27. package/front_end/panels/console/console.ts +3 -0
  28. package/front_end/panels/elements/AdoptedStyleSheetTreeElement.ts +0 -1
  29. package/front_end/panels/elements/ElementsTreeElement.ts +53 -61
  30. package/front_end/panels/elements/PropertiesWidget.ts +1 -1
  31. package/front_end/panels/emulation/DeviceModeToolbar.ts +152 -119
  32. package/front_end/panels/sources/WatchExpressionsSidebarPane.ts +12 -4
  33. package/front_end/panels/timeline/components/liveMetricsView.css +2 -2
  34. package/front_end/third_party/chromium/README.chromium +1 -1
  35. package/front_end/ui/components/text_editor/TextEditor.ts +1 -3
  36. package/front_end/{core → ui}/dom_extension/DOMExtension.ts +1 -1
  37. package/front_end/ui/legacy/Treeoutline.ts +5 -1
  38. package/front_end/ui/legacy/Widget.ts +1 -1
  39. package/front_end/ui/visual_logging/KnownContextValues.ts +4 -0
  40. package/package.json +1 -1
  41. /package/front_end/{core → ui}/dom_extension/dom_extension.ts +0 -0
@@ -7639,6 +7639,14 @@ export const NativeFunctions = [
7639
7639
  name: "Sanitizer",
7640
7640
  signatures: [["?configuration"]]
7641
7641
  },
7642
+ {
7643
+ name: "allowProcessingInstruction",
7644
+ signatures: [["pi"]]
7645
+ },
7646
+ {
7647
+ name: "removeProcessingInstruction",
7648
+ signatures: [["pi"]]
7649
+ },
7642
7650
  {
7643
7651
  name: "TaskController",
7644
7652
  signatures: [["?init"]]
@@ -26,6 +26,10 @@ export function getInsight<InsightName extends keyof InsightModels>(
26
26
  return insightSet.model[insightName];
27
27
  }
28
28
 
29
+ export function isInsightKey(key: string): key is InsightKeys {
30
+ return Object.values(InsightKeys).includes(key as InsightKeys);
31
+ }
32
+
29
33
  export function getLCP(insightSet: InsightSet):
30
34
  {value: Types.Timing.Micro, event: Types.Events.AnyLargestContentfulPaintCandidate}|null {
31
35
  const insight = getInsight(InsightKeys.LCP_BREAKDOWN, insightSet);
@@ -145,7 +145,7 @@ export type InsightModelErrors = {
145
145
  */
146
146
  export type TraceInsightSets = Map<Types.Events.NavigationId, InsightSet>;
147
147
 
148
- export const enum InsightKeys {
148
+ export enum InsightKeys {
149
149
  LCP_BREAKDOWN = 'LCPBreakdown',
150
150
  INP_BREAKDOWN = 'INPBreakdown',
151
151
  CLS_CULPRITS = 'CLSCulprits',
@@ -90,6 +90,7 @@ export interface Call {
90
90
  tool: Tool;
91
91
  input: string;
92
92
  result?: Result;
93
+ cancel: () => void;
93
94
  }
94
95
 
95
96
  export class Tool {
@@ -244,7 +245,16 @@ export class WebMCPModel extends SDK.SDKModel.SDKModel<EventTypes> implements Pr
244
245
  if (!tool) {
245
246
  return;
246
247
  }
247
- const call: Call = {tool, input: params.input, invocationId: params.invocationId};
248
+ const call: Call = {
249
+ tool,
250
+ input: params.input,
251
+ invocationId: params.invocationId,
252
+ cancel: () => {
253
+ if (call.result === undefined) {
254
+ void this.agent.invoke_cancelInvocation({invocationId: params.invocationId});
255
+ }
256
+ },
257
+ };
248
258
  this.#calls.set(params.invocationId, call);
249
259
  this.dispatchEventToListeners(Events.TOOL_INVOKED, call);
250
260
  }
@@ -20,6 +20,7 @@ import * as AiAssistanceModel from '../../../models/ai_assistance/ai_assistance.
20
20
  import * as ComputedStyle from '../../../models/computed_style/computed_style.js';
21
21
  import * as Trace from '../../../models/trace/trace.js';
22
22
  import * as PanelsCommon from '../../../panels/common/common.js';
23
+ import * as TraceBounds from '../../../services/trace_bounds/trace_bounds.js';
23
24
  import * as Marked from '../../../third_party/marked/marked.js';
24
25
  import * as Buttons from '../../../ui/components/buttons/buttons.js';
25
26
  import * as Input from '../../../ui/components/input/input.js';
@@ -31,6 +32,7 @@ import * as Lit from '../../../ui/lit/lit.js';
31
32
  import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';
32
33
  import * as Elements from '../../elements/elements.js';
33
34
  import * as TimelineComponents from '../../timeline/components/components.js';
35
+ import type {BaseInsightComponent} from '../../timeline/components/insights/BaseInsightComponent.js';
34
36
  import * as TimelineInsights from '../../timeline/components/insights/insights.js';
35
37
  import * as Timeline from '../../timeline/timeline.js';
36
38
  import * as TimelineUtils from '../../timeline/utils/utils.js';
@@ -201,6 +203,18 @@ const UIStringsNotTranslate = {
201
203
  * @description Accessible label for the reveal button in the LCP breakdown widget.
202
204
  */
203
205
  revealLcpBreakdown: 'Reveal LCP breakdown',
206
+ /**
207
+ * @description Accessible label for the reveal button in the LCP discovery widget.
208
+ */
209
+ revealLcpDiscovery: 'Reveal LCP discovery',
210
+ /**
211
+ * @description Accessible label for the reveal button in the layout shift culprits widget.
212
+ */
213
+ revealClsCulprits: 'Reveal layout shift culprits',
214
+ /**
215
+ * @description Accessible label for the reveal button in the render-blocking requests widget.
216
+ */
217
+ revealRenderBlockingBreakdown: 'Reveal render-blocking requests',
204
218
  /**
205
219
  * @description Accessible label for the reveal button in the LCP element widget.
206
220
  */
@@ -221,6 +235,18 @@ const UIStringsNotTranslate = {
221
235
  * @description Title for the LCP breakdown widget.
222
236
  */
223
237
  lcpBreakdown: 'LCP breakdown',
238
+ /**
239
+ * @description Title for the LCP discovery widget.
240
+ */
241
+ lcpDiscovery: 'LCP discovery',
242
+ /**
243
+ * @description Title for the layout shift culprits widget.
244
+ */
245
+ clsCulprits: 'Layout shift culprits',
246
+ /**
247
+ * @description Title for the render-blocking requests widget.
248
+ */
249
+ renderBlockingBreakdown: 'Render-blocking requests',
224
250
  /**
225
251
  * @description Title for the LCP element widget.
226
252
  */
@@ -914,33 +940,78 @@ async function makeStylePropertiesWidget(widgetData: StylePropertiesAiWidget): P
914
940
  };
915
941
  }
916
942
 
943
+ const INSIGHT_METADATA: Record<string, {
944
+ component: typeof TimelineInsights.LCPBreakdown.LCPBreakdown | typeof TimelineInsights.RenderBlocking.RenderBlocking |
945
+ typeof TimelineInsights.LCPDiscovery.LCPDiscovery | typeof TimelineInsights.CLSCulprits.CLSCulprits,
946
+ accessibleLabel: string,
947
+ title: string,
948
+ jslog: string,
949
+ }> = {
950
+ [Trace.Insights.Types.InsightKeys.LCP_BREAKDOWN]: {
951
+ component: TimelineInsights.LCPBreakdown.LCPBreakdown,
952
+ accessibleLabel: UIStringsNotTranslate.revealLcpBreakdown,
953
+ title: UIStringsNotTranslate.lcpBreakdown,
954
+ jslog: 'lcp-breakdown-widget',
955
+ },
956
+ [Trace.Insights.Types.InsightKeys.RENDER_BLOCKING]: {
957
+ component: TimelineInsights.RenderBlocking.RenderBlocking,
958
+ accessibleLabel: UIStringsNotTranslate.revealRenderBlockingBreakdown,
959
+ title: UIStringsNotTranslate.renderBlockingBreakdown,
960
+ jslog: 'render-blocking-widget',
961
+ },
962
+ [Trace.Insights.Types.InsightKeys.LCP_DISCOVERY]: {
963
+ component: TimelineInsights.LCPDiscovery.LCPDiscovery,
964
+ accessibleLabel: UIStringsNotTranslate.revealLcpDiscovery,
965
+ title: UIStringsNotTranslate.lcpDiscovery,
966
+ jslog: 'lcp-discovery-widget',
967
+ },
968
+ [Trace.Insights.Types.InsightKeys.CLS_CULPRITS]: {
969
+ component: TimelineInsights.CLSCulprits.CLSCulprits,
970
+ accessibleLabel: UIStringsNotTranslate.revealClsCulprits,
971
+ title: UIStringsNotTranslate.clsCulprits,
972
+ jslog: 'cls-culprits-widget',
973
+ },
974
+ };
975
+
976
+ function renderInsightWidget<T extends Trace.Insights.Types.InsightModel>(
977
+ component: new () => BaseInsightComponent<T>, insight: T, jslog: string, accessibleLabel: string, title: string,
978
+ bounds?: Trace.Types.Timing.TraceWindowMicro): WidgetMakerResponse {
979
+ const renderedWidget = html`<devtools-widget
980
+ class=${jslog}
981
+ ${widget(component, {
982
+ model: insight,
983
+ minimal: true,
984
+ bounds: bounds ?? null,
985
+ })}></devtools-widget>`;
986
+
987
+ return {
988
+ renderedWidget,
989
+ revealable: new TimelineUtils.Helpers.RevealableInsight(insight),
990
+ accessibleRevealLabel: lockedString(accessibleLabel),
991
+ title: lockedString(title),
992
+ jslogContext: jslog,
993
+ };
994
+ }
995
+
917
996
  async function makePerfInsightWidget(widgetData: PerfInsightAiWidget): Promise<WidgetMakerResponse|null> {
918
- switch (widgetData.data.insight) {
919
- case 'lcp': {
920
- const insight = widgetData.data.insightData;
921
- if (!insight || !Trace.Insights.Models.LCPBreakdown.isLCPBreakdownInsight(insight)) {
922
- return null;
923
- }
924
- // clang-format off
925
- const renderedWidget = html`<devtools-widget
926
- class="lcp-breakdown-widget"
927
- ${widget(TimelineInsights.LCPBreakdown.LCPBreakdown, {
928
- model: insight,
929
- minimal: true,
930
- })}></devtools-widget>`;
931
- // clang-format on
997
+ const insightKey = widgetData.data.insight;
998
+ const insight = widgetData.data.insightData;
932
999
 
933
- return {
934
- renderedWidget,
935
- revealable: new TimelineUtils.Helpers.RevealableInsight(insight),
936
- accessibleRevealLabel: lockedString(UIStringsNotTranslate.revealLcpBreakdown),
937
- title: lockedString(UIStringsNotTranslate.lcpBreakdown),
938
- jslogContext: 'lcp-breakdown',
939
- };
940
- }
941
- default:
1000
+ const meta = INSIGHT_METADATA[insightKey];
1001
+ if (!meta) {
1002
+ return null;
1003
+ }
1004
+
1005
+ let bounds;
1006
+ if (insightKey === Trace.Insights.Types.InsightKeys.CLS_CULPRITS) {
1007
+ const traceBounds = TraceBounds.TraceBounds.BoundsManager.instance().state()?.micro.entireTraceBounds;
1008
+ if (!traceBounds) {
942
1009
  return null;
1010
+ }
1011
+ bounds = traceBounds;
943
1012
  }
1013
+
1014
+ return renderInsightWidget(meta.component, insight, meta.jslog, meta.accessibleLabel, meta.title, bounds);
944
1015
  }
945
1016
 
946
1017
  async function makeBottomUpTimelineTreeWidget(widgetData: BottomUpTreeAiWidget): Promise<WidgetMakerResponse|null> {
@@ -167,6 +167,10 @@ const UIStrings = {
167
167
  * @description Context menu action to copy the description of a tool
168
168
  */
169
169
  copyDescription: 'Copy description',
170
+ /**
171
+ * @description Context menu action to cancel an in-progress tool call
172
+ */
173
+ cancelCall: 'Cancel',
170
174
  /**
171
175
  * @description Text for the header of the tool run section
172
176
  */
@@ -498,6 +502,11 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
498
502
  const payload = parsePayload(call.input);
499
503
  input.onRevealTool(call.tool, payload.valueObject as Record<string, unknown> | undefined);
500
504
  }, {jslogContext: 'webmcp.edit-and-run', disabled: isUnregistered});
505
+ if (call.result === undefined) {
506
+ contextMenu.defaultSection().appendItem(i18nString(UIStrings.cancelCall), () => {
507
+ call.cancel();
508
+ }, {jslogContext: 'webmcp.cancel-call'});
509
+ }
501
510
  }}>
502
511
  <td>
503
512
  <div class="name-cell">
@@ -598,11 +607,11 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
598
607
  ` : html`
599
608
  <devtools-list class="square-corners">
600
609
  ${tools.map(tool => html`
601
- <div class=${Directives.classMap({'tool-item': true, selected: tool === input.selectedTool?.tool})}
602
- @click=${() => input.onToolSelect(tool)}
603
- @contextmenu=${(e: Event) => onToolContextMenu(e, tool)}>
604
- <div class="tool-name-container">
605
- <div class="tool-name source-code">${tool.name}</div>
610
+ <div class=${Directives.classMap({'tool-item': true, selected: tool === input.selectedTool?.tool})}
611
+ @click=${() => input.onToolSelect(tool)}
612
+ @contextmenu=${(e: Event) => onToolContextMenu(e, tool)}>
613
+ <div class="tool-name-container">
614
+ <div class="tool-name source-code">${tool.name}</div>
606
615
  <div class="tool-icons">
607
616
  ${getIconGroupsFromStats(toolStats.stats.get(tool)).map(group => html`
608
617
  <icon-button
@@ -613,8 +622,8 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
613
622
  } as IconButton.IconButton.IconButtonData}
614
623
  @click=${(e: Event) => e.stopPropagation()}></icon-button>`)}
615
624
  </div>
616
- </div>
617
- <div class="tool-description">${tool.description}</div>
625
+ </div>
626
+ <div class="tool-description">${tool.description}</div>
618
627
  </div>`)}
619
628
  </devtools-list>
620
629
  `}
@@ -0,0 +1,69 @@
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 Bindings from '../../models/bindings/bindings.js';
6
+ import type * as Workspace from '../../models/workspace/workspace.js';
7
+ import * as UI from '../../ui/legacy/legacy.js';
8
+
9
+ export interface ViewInput {
10
+ error: Bindings.SymbolizedError.SymbolizedError;
11
+ ignoreListManager?: Workspace.IgnoreListManager.IgnoreListManager;
12
+ }
13
+
14
+ const DEFAULT_VIEW = (_input: ViewInput, _output: object, _target: HTMLElement): void => {};
15
+
16
+ export class SymbolizedErrorWidget extends UI.Widget.Widget {
17
+ #error?: Bindings.SymbolizedError.SymbolizedError;
18
+ #view: typeof DEFAULT_VIEW;
19
+ #ignoreListManager?: Workspace.IgnoreListManager.IgnoreListManager;
20
+
21
+ constructor(element?: HTMLElement, view: typeof DEFAULT_VIEW = DEFAULT_VIEW) {
22
+ super(element);
23
+ this.#view = view;
24
+ }
25
+
26
+ set ignoreListManager(ignoreListManager: Workspace.IgnoreListManager.IgnoreListManager) {
27
+ this.#ignoreListManager = ignoreListManager;
28
+ this.requestUpdate();
29
+ }
30
+
31
+ get ignoreListManager(): Workspace.IgnoreListManager.IgnoreListManager|undefined {
32
+ return this.#ignoreListManager;
33
+ }
34
+
35
+ set error(error: Bindings.SymbolizedError.SymbolizedError) {
36
+ this.#error?.removeEventListener(Bindings.SymbolizedError.Events.UPDATED, this.requestUpdate, this);
37
+ this.#error = error;
38
+ if (this.isShowing()) {
39
+ this.#error?.addEventListener(Bindings.SymbolizedError.Events.UPDATED, this.requestUpdate, this);
40
+ }
41
+ this.requestUpdate();
42
+ }
43
+
44
+ get error(): Bindings.SymbolizedError.SymbolizedError|undefined {
45
+ return this.#error;
46
+ }
47
+
48
+ override wasShown(): void {
49
+ super.wasShown();
50
+ this.#error?.addEventListener(Bindings.SymbolizedError.Events.UPDATED, this.requestUpdate, this);
51
+ this.requestUpdate();
52
+ }
53
+
54
+ override willHide(): void {
55
+ super.willHide();
56
+ this.#error?.removeEventListener(Bindings.SymbolizedError.Events.UPDATED, this.requestUpdate, this);
57
+ }
58
+
59
+ override performUpdate(): void {
60
+ if (!this.#error) {
61
+ return;
62
+ }
63
+ const input: ViewInput = {
64
+ error: this.#error,
65
+ ignoreListManager: this.#ignoreListManager,
66
+ };
67
+ this.#view(input, {}, this.contentElement);
68
+ }
69
+ }
@@ -13,6 +13,7 @@ import './ConsoleViewMessage.js';
13
13
  import './ConsolePrompt.js';
14
14
  import './ConsoleView.js';
15
15
  import './ConsolePanel.js';
16
+ import './SymbolizedErrorWidget.js';
16
17
  import './PromptBuilder.js';
17
18
 
18
19
  import * as ConsoleContextSelector from './ConsoleContextSelector.js';
@@ -27,6 +28,7 @@ import * as ConsoleView from './ConsoleView.js';
27
28
  import * as ConsoleViewMessage from './ConsoleViewMessage.js';
28
29
  import * as ConsoleViewport from './ConsoleViewport.js';
29
30
  import * as PromptBuilder from './PromptBuilder.js';
31
+ import * as SymbolizedErrorWidget from './SymbolizedErrorWidget.js';
30
32
 
31
33
  export {
32
34
  ConsoleContextSelector,
@@ -41,4 +43,5 @@ export {
41
43
  ConsoleViewMessage,
42
44
  ConsoleViewport,
43
45
  PromptBuilder,
46
+ SymbolizedErrorWidget,
44
47
  };
@@ -154,7 +154,6 @@ export class AdoptedStyleSheetContentsTreeElement extends UI.TreeOutline.TreeEle
154
154
  this.editing = {
155
155
  commit: editorHandles.commit,
156
156
  cancel: editorHandles.cancel,
157
- editor: undefined,
158
157
  resize: () => {},
159
158
  };
160
159
 
@@ -460,10 +460,14 @@ export interface ViewInput {
460
460
  descendantDecorations: Decoration[];
461
461
  decorationsTooltip: string;
462
462
  indent: number;
463
+
464
+ editorState: CodeMirror.EditorState|null;
465
+ editorWidth: number|null;
463
466
  }
464
467
 
465
468
  export interface ViewOutput {
466
469
  contentElement?: HTMLElement;
470
+ editorRef?: TextEditor.TextEditor.TextEditor;
467
471
  }
468
472
 
469
473
  export function adornerRef(): DirectiveResult<typeof Lit.Directives.RefDirective> {
@@ -936,11 +940,12 @@ export const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLE
936
940
  const gutterContainerClasses = {
937
941
  'has-decorations': input.decorations.length || input.descendantDecorations.length,
938
942
  'gutter-container': true,
943
+ hidden: Boolean(input.editorState),
939
944
  };
940
945
  // clang-format off
941
946
  render(html`
942
947
  <div ${ref(el => { output.contentElement = el as HTMLElement; })}>
943
- ${input.node ? html`<span class="highlight">${renderTitle(
948
+ ${input.node ? html`<span class="highlight ${input.editorState ? 'hidden' : ''}">${renderTitle(
944
949
  input.node,
945
950
  input.isClosingTag,
946
951
  input.expanded,
@@ -951,7 +956,7 @@ export const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLE
951
956
  input.onExpand,
952
957
  )}</span>` : nothing}
953
958
  ${input.isHovered || input.isSelected ? html`
954
- <div class="selection fill" style=${`margin-left: ${-input.indent}px`}></div>
959
+ <div class="selection fill ${input.editorState ? 'hidden' : ''}" style=${`margin-left: ${-input.indent}px`}></div>
955
960
  ` : nothing}
956
961
  <div class=${Lit.Directives.classMap(gutterContainerClasses)}
957
962
  style="left: ${-input.indent}px"
@@ -964,7 +969,7 @@ export const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLE
964
969
  ${input.descendantDecorations.map(d => html`<div class="elements-gutter-decoration elements-has-decorated-children" style="--decoration-color: ${d.color}"></div>`)}
965
970
  </div>` : nothing}
966
971
  </div>
967
- ${hasAdorners ? html`<div class="adorner-container ${!hasAdorners ? 'hidden' : ''}">
972
+ ${hasAdorners ? html`<div class="adorner-container ${(input.editorState) ? 'hidden' : ''}">
968
973
  ${maybeRenderAdAdorner(input)}
969
974
  ${input.showViewSourceAdorner ? html`<devtools-adorner
970
975
  .name=${ElementsComponents.AdornerManager.RegisteredAdorners.VIEW_SOURCE}
@@ -1123,10 +1128,10 @@ export const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLE
1123
1128
  </devtools-adorner>` : nothing}
1124
1129
  </div>`: nothing}
1125
1130
  ${input.isSelected ? html`
1126
- <span class="selected-hint" title=${i18nString(UIStrings.useSInTheConsoleToReferToThis, { PH1: '$0' })} aria-hidden="true"></span>
1131
+ <span class="selected-hint ${input.editorState ? 'hidden' : ''}" title=${i18nString(UIStrings.useSInTheConsoleToReferToThis, { PH1: '$0' })} aria-hidden="true"></span>
1127
1132
  ` : nothing}
1128
1133
  ${input.showAiButton ? html`
1129
- <span class="ai-button-container">
1134
+ <span class="ai-button-container ${input.editorState ? 'hidden' : ''}">
1130
1135
  <devtools-floating-button
1131
1136
  icon-name=${AIAssistance.AiUtils.getIconName()}
1132
1137
  title=${input.aiButtonTitle || ''}
@@ -1136,6 +1141,15 @@ export const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLE
1136
1141
  </devtools-floating-button>
1137
1142
  </span>
1138
1143
  ` : nothing}
1144
+ ${input.editorState ? html`<div @keydown=${(event: KeyboardEvent) => {
1145
+ if (event.key === 'Escape') {
1146
+ event.consume(true);
1147
+ }
1148
+ }} class="source-code elements-tree-editor" style="width: ${input.editorWidth ?? 0}px;">
1149
+ <devtools-text-editor .state=${input.editorState} ${ref(el => {
1150
+ output.editorRef = el as TextEditor.TextEditor.TextEditor;
1151
+ })}></devtools-text-editor>
1152
+ </div>`: nothing}
1139
1153
  </div>
1140
1154
  `, target);
1141
1155
  // clang-format on
@@ -1151,7 +1165,9 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
1151
1165
  private inClipboard: boolean;
1152
1166
  #hovered: boolean;
1153
1167
  private editing: EditorHandles|null;
1154
- private htmlEditElement?: HTMLElement;
1168
+ #editorRef?: TextEditor.TextEditor.TextEditor;
1169
+ #editorState: CodeMirror.EditorState|null = null;
1170
+ #editorWidth: number|null = null;
1155
1171
  expandAllButtonElement: UI.TreeOutline.TreeElement|null;
1156
1172
  #elementIssues = new Map<string, IssuesManager.Issue.Issue>();
1157
1173
  #nodeElementToIssue = new Map<Element, IssuesManager.Issue.Issue[]>();
@@ -1288,9 +1304,6 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
1288
1304
 
1289
1305
  // ClearNode param is used to clean DOM after in-place editing..
1290
1306
  performUpdate(clearNode = false): void {
1291
- if (this.editing) {
1292
- return;
1293
- }
1294
1307
  const output: ViewOutput = {};
1295
1308
  DEFAULT_VIEW(
1296
1309
  {
@@ -1380,10 +1393,13 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
1380
1393
  void action.execute();
1381
1394
  }
1382
1395
  },
1396
+ editorState: this.#editorState,
1397
+ editorWidth: this.#editorWidth,
1383
1398
  },
1384
1399
  output, this.listItemElement);
1385
1400
 
1386
1401
  this.#contentElement = output.contentElement;
1402
+ this.#editorRef = output.editorRef;
1387
1403
  if (this.#updateRecord) {
1388
1404
  this.#updateRecord = null;
1389
1405
  }
@@ -1683,10 +1699,7 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
1683
1699
  }
1684
1700
  }
1685
1701
 
1686
- override onunbind(): void {
1687
- if (this.editing) {
1688
- this.editing.cancel();
1689
- }
1702
+ clearView(): void {
1690
1703
  // Update the element to clean up adorner registrations with the
1691
1704
  // ElementsPanel.
1692
1705
  // We do not change the ElementsTreeElement state in case the
@@ -1744,9 +1757,17 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
1744
1757
  descendantDecorations: [],
1745
1758
  decorationsTooltip: '',
1746
1759
  indent: 0,
1760
+ editorState: null,
1761
+ editorWidth: null,
1747
1762
  },
1748
1763
  {}, this.listItemElement);
1764
+ }
1749
1765
 
1766
+ override onunbind(): void {
1767
+ if (this.editing) {
1768
+ this.editing.cancel();
1769
+ }
1770
+ this.clearView();
1750
1771
  if (this.treeOutline && this.treeOutline.treeElementByNode.get(this.nodeInternal) === this) {
1751
1772
  this.treeOutline.treeElementByNode.delete(this.nodeInternal);
1752
1773
  }
@@ -2553,7 +2574,6 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
2553
2574
  this.editing = {
2554
2575
  commit: editorHandles.commit,
2555
2576
  cancel: editorHandles.cancel,
2556
- editor: undefined,
2557
2577
  resize: () => {},
2558
2578
  };
2559
2579
  }
@@ -2569,31 +2589,13 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
2569
2589
  return;
2570
2590
  }
2571
2591
 
2572
- const initialValue = convertUnicodeCharsToHTMLEntities(maybeInitialValue).text;
2573
- this.htmlEditElement = document.createElement('div');
2574
- this.htmlEditElement.className = 'source-code elements-tree-editor';
2575
-
2576
- // Hide header items.
2577
- let child: (ChildNode|null) = this.listItemElement.firstChild;
2578
- while (child) {
2579
- if (child instanceof HTMLElement) {
2580
- child.style.display = 'none';
2581
- }
2582
- child = child.nextSibling;
2583
- }
2584
2592
  // Hide children item.
2585
2593
  if (this.childrenListElement) {
2586
2594
  this.childrenListElement.style.display = 'none';
2587
2595
  }
2596
+ const initialValue = convertUnicodeCharsToHTMLEntities(maybeInitialValue).text;
2588
2597
  // Append editor.
2589
- this.listItemElement.append(this.htmlEditElement);
2590
- this.htmlEditElement.addEventListener('keydown', event => {
2591
- if (event.key === 'Escape') {
2592
- event.consume(true);
2593
- }
2594
- });
2595
-
2596
- const editor = new TextEditor.TextEditor.TextEditor(CodeMirror.EditorState.create({
2598
+ this.#editorState = CodeMirror.EditorState.create({
2597
2599
  doc: initialValue,
2598
2600
  extensions: [
2599
2601
  CodeMirror.keymap.of([
@@ -2623,58 +2625,49 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
2623
2625
  }),
2624
2626
  CodeMirror.EditorView.domEventHandlers({
2625
2627
  focusout: event => {
2628
+ if (!this.#editorRef) {
2629
+ return;
2630
+ }
2626
2631
  // The relatedTarget is null when no element gains focus, e.g. switching windows.
2627
2632
  const relatedTarget = (event.relatedTarget as Node | null);
2628
- if (relatedTarget && !relatedTarget.isSelfOrDescendant(editor)) {
2633
+ if (relatedTarget && !relatedTarget.isSelfOrDescendant(this.#editorRef)) {
2629
2634
  this.editing?.commit();
2630
2635
  }
2631
2636
  },
2632
2637
  }),
2633
2638
  ],
2634
- }));
2635
- this.editing = {commit: commit.bind(this), cancel: dispose.bind(this), editor, resize: resize.bind(this)};
2639
+ });
2640
+ this.performUpdate();
2636
2641
  resize.call(this);
2637
- this.htmlEditElement.appendChild(editor);
2638
- editor.editor.focus();
2639
-
2642
+ this.#editorRef?.focus();
2643
+ this.editing = {commit: commit.bind(this), cancel: dispose.bind(this), resize: resize.bind(this)};
2640
2644
  this.treeOutline?.setMultilineEditing(this.editing);
2641
2645
 
2642
2646
  function resize(this: ElementsTreeElement): void {
2643
- if (this.treeOutline && this.htmlEditElement) {
2644
- this.htmlEditElement.style.width = this.treeOutline.visibleWidth() - this.computeLeftIndent() - 30 + 'px';
2647
+ if (this.treeOutline) {
2648
+ this.#editorWidth = this.treeOutline.visibleWidth() - this.computeLeftIndent() - 30;
2649
+ this.performUpdate();
2645
2650
  }
2646
2651
  }
2647
2652
 
2648
2653
  function commit(this: ElementsTreeElement): void {
2649
- if (this.editing?.editor) {
2650
- commitCallback(initialValue, this.editing.editor.state.doc.toString());
2654
+ if (this.#editorRef) {
2655
+ commitCallback(initialValue, this.#editorRef.editor.state.doc.toString());
2651
2656
  }
2652
2657
  dispose.call(this);
2653
2658
  }
2654
2659
 
2655
2660
  function dispose(this: ElementsTreeElement): void {
2656
- if (!this.editing?.editor) {
2661
+ if (!this.#editorRef) {
2657
2662
  return;
2658
2663
  }
2659
2664
  this.editing = null;
2660
-
2661
- // Remove editor.
2662
- if (this.htmlEditElement) {
2663
- this.listItemElement.removeChild(this.htmlEditElement);
2664
- }
2665
- this.htmlEditElement = undefined;
2665
+ this.#editorState = null;
2666
+ this.performUpdate();
2666
2667
  // Unhide children item.
2667
2668
  if (this.childrenListElement) {
2668
2669
  this.childrenListElement.style.removeProperty('display');
2669
2670
  }
2670
- // Unhide header items.
2671
- let child: (ChildNode|null) = this.listItemElement.firstChild;
2672
- while (child) {
2673
- if (child instanceof HTMLElement) {
2674
- child.style.removeProperty('display');
2675
- }
2676
- child = child.nextSibling;
2677
- }
2678
2671
 
2679
2672
  if (this.treeOutline) {
2680
2673
  this.treeOutline.setMultilineEditing(null);
@@ -3016,7 +3009,7 @@ export class ElementsTreeElement extends UI.TreeOutline.TreeElement {
3016
3009
  }
3017
3010
 
3018
3011
  toggleEditAsHTML(callback?: ((arg0: boolean) => void), startEditing?: boolean): void {
3019
- if (this.editing && this.htmlEditElement) {
3012
+ if (this.editing && this.#editorState) {
3020
3013
  this.editing.commit();
3021
3014
  return;
3022
3015
  }
@@ -3222,7 +3215,6 @@ export function convertUnicodeCharsToHTMLEntities(text: string): {
3222
3215
  export interface EditorHandles {
3223
3216
  commit: () => void;
3224
3217
  cancel: () => void;
3225
- editor?: TextEditor.TextEditor.TextEditor;
3226
3218
  resize: () => void;
3227
3219
  }
3228
3220
 
@@ -103,7 +103,7 @@ export const DEFAULT_VIEW: View = (input, _output, target) => {
103
103
  ${input.objectTree && input.allChildrenFiltered ? html`
104
104
  <div class="gray-info-message">${i18nString(UIStrings.noMatchingProperty)}</div>
105
105
  ` : nothing}
106
- <devtools-tree @treeelementexpand=${onExpand} .template=${html`
106
+ <devtools-tree show-selection-on-keyboard-focus @treeelementexpand=${onExpand} .template=${html`
107
107
  <ul role=tree class="source-code object-properties-section">
108
108
  <style>${ObjectUI.ObjectPropertiesSection.objectValueStyles}</style>;
109
109
  <style>${ObjectUI.ObjectPropertiesSection.objectPropertiesSectionStyles}</style>;