chrome-devtools-frontend 1.0.1596535 → 1.0.1597448

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 (29) 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/InspectorFrontendHostAPI.ts +1 -0
  4. package/front_end/core/host/UserMetrics.ts +12 -0
  5. package/front_end/core/root/Runtime.ts +5 -0
  6. package/front_end/core/sdk/CPUThrottlingManager.ts +9 -12
  7. package/front_end/core/sdk/PageResourceLoader.ts +22 -1
  8. package/front_end/devtools_compatibility.js +2 -1
  9. package/front_end/models/ai_assistance/AiConversation.ts +5 -0
  10. package/front_end/models/ai_assistance/agents/AiAgent.ts +4 -0
  11. package/front_end/models/ai_assistance/agents/StylingAgent.snapshot.txt +24 -0
  12. package/front_end/models/ai_assistance/agents/StylingAgent.ts +289 -12
  13. package/front_end/models/greendev/Prototypes.ts +7 -1
  14. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +35 -33
  15. package/front_end/panels/ai_assistance/PatchWidget.ts +6 -6
  16. package/front_end/panels/ai_assistance/components/ChatMessage.ts +3 -28
  17. package/front_end/panels/ai_assistance/components/ChatView.ts +6 -11
  18. package/front_end/panels/ai_assistance/components/StylingAgentMarkdownRenderer.ts +200 -0
  19. package/front_end/panels/ai_assistance/components/chatMessage.css +0 -8
  20. package/front_end/panels/application/ServiceWorkerCacheViews.ts +1 -1
  21. package/front_end/panels/common/ExtensionServer.ts +15 -0
  22. package/front_end/panels/elements/ElementsTreeElement.ts +55 -47
  23. package/front_end/panels/elements/ElementsTreeOutline.ts +149 -28
  24. package/front_end/panels/lighthouse/LighthousePanel.ts +8 -0
  25. package/front_end/panels/settings/SettingsScreen.ts +3 -2
  26. package/front_end/ui/legacy/UIUtils.ts +5 -5
  27. package/front_end/ui/legacy/Widget.ts +33 -2
  28. package/front_end/ui/visual_logging/KnownContextValues.ts +1 -0
  29. package/package.json +1 -1
@@ -11,7 +11,7 @@ Adhere strictly to the Model-View-Presenter (MVP) architecture.
11
11
  ### Presenter (`Widget`) Rules
12
12
 
13
13
  * Location: Co-located in the same file as its View.
14
- * MUST extend a base `UI.Widget` class (e.g., `UI.Widget.Widget`). Note that `UI.Widget.Widget` is not an `HTMLElement` and must be appended via `.show()` or `<devtools-widget>`.
14
+ * MUST extend a base `UI.Widget` class (e.g., `UI.Widget.Widget`). Note that `UI.Widget.Widget` is not an `HTMLElement` and must be appended via `.show()` or `UI.Widget.widget`
15
15
  * Constructor MUST assign the injected view function to a private `#view` field.
16
16
  * Constructor MUST call `super()`. If taking `element?: HTMLElement`, pass it to `super(element)`. `super(true)` is forbidden.
17
17
  * Styling MUST be handled within the View. `this.registerCSSFiles()` is forbidden.
@@ -50,10 +50,10 @@ const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLElement)
50
50
  // clang-format on
51
51
  ```
52
52
 
53
- ### Composition (`<devtools-widget>`)
53
+ ### Composition
54
54
 
55
- * To render a child widget, the parent's View MUST use `<devtools-widget>`.
56
- * Configuration is passed via the `.widgetConfig` property using `UI.Widget.widgetConfig()`.
55
+ * To render a child widget, the parent's View MUST use lit-html directive `UI.Widget.wiget`
56
+ * Configuration is passed via the parameters.
57
57
  * Properties passed from a parent MUST be declared as public fields on the child presenter class.
58
58
  * The framework automatically updates these properties and calls `requestUpdate()` on the child when the parent re-renders.
59
59
 
@@ -66,7 +66,7 @@ When migrating imperative components (extending `UI.VBox`, `UI.Panel`, or `HTMLE
66
66
  * Prefer extending `UI.Widget.Widget`.
67
67
  * **Special Case:** If the component *must* extend `UI.Panel.Panel` or `UI.Dialog.Dialog` (to retain specific functionality), you cannot use `requestUpdate()`. Instead, call `this.performUpdate()` directly on state changes.
68
68
  3. **State Migration:** Move DOM-stored state to private class fields.
69
- 4. **Update Usage:** Replace `new MyComponent()` instantiations with declarative `<devtools-widget .widgetConfig=...>` in parent templates.
69
+ 4. **Update Usage:** Replace `new MyComponent()` instantiations with declarative `widget(MyComponent, {params})` in parent templates.
70
70
  5. **Scoping Styles:** Ensure your CSS file uses the `@scope` block to prevent style leaks:
71
71
  ```css
72
72
  @scope to (devtools-widget > *) {
@@ -101,7 +101,7 @@ render(html`
101
101
 
102
102
  ### Legacy Interop & Refs
103
103
  * **Raw Elements:** Use `Lit.Directives.ref` to obtain a reference to a raw `HTMLElement` if needed for imperative APIs (e.g., `splitWidget.installResizer(element)`).
104
- * **Child Widgets:** Use `UI.Widget.widgetRef` to obtain the class instance of a child `<devtools-widget>` if you need to call methods on it directly (though declarative data flow is preferred).
104
+ * **Child Widgets:** Use `UI.Widget.widgetRef` to obtain the class instance of a child widget if you need to call methods on it directly (though declarative data flow is preferred).
105
105
 
106
106
  ### Dependencies
107
107
  The `DEFAULT_VIEW` is typically a module-level function. Ensure all dependencies (enums, constants, other components) are imported at the top of the file so they are available in the function scope.
@@ -236,8 +236,7 @@ const DEFAULT_VIEW = (input: ViewInput, output: undefined, target: HTMLElement):
236
236
  <button @click=${input.onTitleChange}>Change Child Title</button>
237
237
 
238
238
  <!-- Pass properties to the child widget. -->
239
- <devtools-widget .widgetConfig=${widgetConfig(MyExampleWidget, {title: input.title})}>
240
- </devtools-widget>
239
+ ${widget(MyExampleWidget, {title: input.title})}
241
240
  </div>
242
241
  `, target);
243
242
  };
@@ -40,11 +40,11 @@ To test the `DEFAULT_VIEW` function itself, we should use screenshot and e2e tes
40
40
 
41
41
  We should no longer use imperative API to update DOM. Instead we rely on orchestrated rendering of lit-html templates. The view function described above should be a call to lit-html `render`. The view function should be called from `UI.Widget`’s `performUpdate` method, which by default is scheduled using `requestAnimationFrame`.
42
42
 
43
- To embed another presenter (`UI.Widget`) in the lit-html template, use `<devtools-widget .widgetConfig=${widgetConfig(<class>, {foo: 1, bar: 2})}`
43
+ To embed another presenter (`UI.Widget`) in the lit-html template, use `widget(<class>, {foo: 1, bar: 2})`
44
44
 
45
45
  This will instantiate a `Widget` class with the web component as its `element` and, optionally, will set the properties provided in the second parameter. The widget won’t be re-instantiated on the subsequent template renders, but the properties would be updated. For this to work, the widget needs to accept `HTMLElement` as a sole constructor parameter and properties need to be public members or setters.
46
46
 
47
- For backwards compatibility, the first argument to `widgetConfig` can also be a factory function: `<devtools-widget .widgetConfig=${widgetConfig(element => new MyWidget(foo, bar, element))}>`. Similar to the class constructor version, `element` is the actual `<devtools-widget>` so the following two invocations of `widgetConfig` are equivalent: `widgetConfig(MyWidget)` and `widgetConfig(element => new MyWidget(element))`.
47
+ For backwards compatibility, the first argument to `widgetConfig` can also be a factory function: `widget(element => new MyWidget(foo, bar, element))`. Similar to the class constructor version, `element` is the actual `<devtools-widget>` so the following two invocations of `widgetConfig` are equivalent: `widget(MyWidget)` and `widget(element => new MyWidget(element))`.
48
48
 
49
49
  ## Styling
50
50
  To prevent style conflicts in widgets without relying on shadow DOM, we use the CSS [`@scope`](https://developer.mozilla.org/en-US/docs/Web/CSS/@scope) at-rule for style encapsulation. This ensures that styles defined for a widget do not leak out and affect other components.
@@ -83,12 +83,12 @@ render(html`
83
83
  <div class="container">
84
84
  <h3 class="title">My Widget</h3>
85
85
  ...
86
- <devtools-widget .widgetConfig=${widgetConfig(NestedWidget)}></devtools-widget>
86
+ ${widget(NestedWidget)}
87
87
  </div>
88
88
  `, this.element);
89
89
  ```
90
90
 
91
- In this example, the `.title` style will apply within the parent widget but will not leak into the nested `<devtools-widget>`. Because this convention relies on developer discipline, it is important to verify its correct application during code reviews.
91
+ In this example, the `.title` style will apply within the parent widget but will not leak into the nested widget. Because this convention relies on developer discipline, it is important to verify its correct application during code reviews.
92
92
 
93
93
  ## Examples
94
94
 
@@ -97,8 +97,8 @@ In this example, the `.title` style will apply within the parent widget but will
97
97
  <devtools-split-view>
98
98
  <devtools-widget slot="main" .widgetConfig=${widgetConfig(ElementsTree)}></devtools-widget>
99
99
  <devtools-tab-pane slot="sidebar">
100
- <devtools-widget .widgetConfig=${widgetConfig(StylesPane, {element: input.element})}></devtools-widget>
101
- <devtools-widget .widgetConfig=${widgetConfig(ComputedPane, {element: input.element})}></devtools-widget>
100
+ ${widget(StylesPane, {element: input.element})}
101
+ ${widget(ComputedPane, {element: input.element})}
102
102
  ...
103
103
  </devtools-tab-pane>
104
104
  </devtools-split-view>
@@ -109,8 +109,7 @@ In this example, the `.title` style will apply within the parent widget but will
109
109
  type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void;
110
110
  const DEFAULT_VIEW = (input, output, target) => {
111
111
  render(html`
112
- <devtools-widget .widgetConfig=${widgetConfig(MetricsPane, {element: input.element})}>
113
- </devtools-widget>
112
+ ${widget(MetricsPane, {element: input.element})}
114
113
  <devtools-toolbar>
115
114
  <devtools-filter-input @change=${input.onFilter}></devtools-filter-input>
116
115
  <devtools-checkbox @change=${input.onShowAll}>Show All</devtools-checkbox>
@@ -1236,7 +1235,7 @@ export const DEFAULT_VIEW = (input, _output, target) => {
1236
1235
 
1237
1236
  ## Migrating `EmptyWidget`
1238
1237
 
1239
- Replace the imperative `EmptyWidget` with a declarative `<devtools-widget>` and configure it with `widgetConfig` to render an `EmptyWidget`.
1238
+ Replace the imperative `EmptyWidget` with a declarative `widget` and configure it to render an `EmptyWidget`.
1240
1239
 
1241
1240
  **Before:**
1242
1241
  ```typescript
@@ -1258,9 +1257,9 @@ class SomeWidget extends UI.Widget.Widget {
1258
1257
  export const DEFAULT_VIEW = (input, _output, target) => {
1259
1258
  render(html`
1260
1259
  <div>
1261
- <devtools-widget .widgetConfig=${widgetConfig(UI.EmptyWidget.EmptyWidget,{
1260
+ ${widget(UI.EmptyWidget.EmptyWidget,{
1262
1261
  header: i18nString(UIStrings.nothingToSeeHere), text: this.explanation,
1263
- link: 'http://www.google.com',})}></devtools-widget>
1262
+ link: 'http://www.google.com',})}
1264
1263
  </div>`,
1265
1264
  target, {host: input});
1266
1265
  };
@@ -556,5 +556,6 @@ export const enum EnumeratedHistogram {
556
556
  LighthouseCategoryUsed = 'DevTools.LighthouseCategoryUsed',
557
557
  SwatchActivated = 'DevTools.SwatchActivated',
558
558
  BuiltInAiAvailability = 'DevTools.BuiltInAiAvailability',
559
+ ExtensionEvalTarget = 'DevTools.ExtensionEvalTarget',
559
560
  // LINT.ThenChange(/front_end/devtools_compatibility.js:EnumeratedHistogram)
560
561
  }
@@ -322,6 +322,11 @@ export class UserMetrics {
322
322
  InspectorFrontendHostInstance.recordPerformanceHistogram(
323
323
  'DevTools.Insights.ShortTeaserGenerationTime', timeInMilliseconds);
324
324
  }
325
+
326
+ extensionEvalTarget(target: ExtensionEvalTarget): void {
327
+ InspectorFrontendHostInstance.recordEnumeratedHistogram(
328
+ EnumeratedHistogram.ExtensionEvalTarget, target, ExtensionEvalTarget.MAX_VALUE);
329
+ }
325
330
  }
326
331
 
327
332
  /**
@@ -1264,3 +1269,10 @@ export const enum BuiltInAiAvailability {
1264
1269
  DISABLED_NO_GPU = 9,
1265
1270
  MAX_VALUE = 10,
1266
1271
  }
1272
+
1273
+ export const enum ExtensionEvalTarget {
1274
+ WEB_PAGE = 0,
1275
+ SAME_EXTENSION = 1,
1276
+ OTHER_EXTENSION = 2,
1277
+ MAX_VALUE = 3,
1278
+ }
@@ -476,6 +476,10 @@ export interface HostConfigAiAssistanceFileAgent {
476
476
  userTier: string;
477
477
  }
478
478
 
479
+ export interface HostConfigAiAssistanceAccessibilityAgent {
480
+ enabled: boolean;
481
+ }
482
+
479
483
  export interface HostConfigAiCodeCompletion {
480
484
  modelId: string;
481
485
  temperature: number;
@@ -632,6 +636,7 @@ export type HostConfig = Platform.TypeScriptUtilities.RecursivePartial<{
632
636
  devToolsAiAssistanceNetworkAgent: HostConfigAiAssistanceNetworkAgent,
633
637
  devToolsAiAssistanceFileAgent: HostConfigAiAssistanceFileAgent,
634
638
  devToolsAiAssistancePerformanceAgent: HostConfigAiAssistancePerformanceAgent,
639
+ devToolsAiAssistanceAccessibilityAgent: HostConfigAiAssistanceAccessibilityAgent,
635
640
  devToolsAiAssistanceV2: HostConfigAiAssistanceV2,
636
641
  devToolsAiCodeCompletion: HostConfigAiCodeCompletion,
637
642
  devToolsAiCodeGeneration: HostConfigAiCodeGeneration,
@@ -39,27 +39,24 @@ let throttlingManagerInstance: CPUThrottlingManager;
39
39
 
40
40
  export class CPUThrottlingManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes> implements
41
41
  SDKModelObserver<EmulationModel> {
42
- readonly #targetManager: TargetManager;
43
42
  #cpuThrottlingOption: CPUThrottlingOption;
44
43
  #calibratedThrottlingSetting: Common.Settings.Setting<CalibratedCPUThrottling>;
45
44
  #hardwareConcurrency?: number;
46
45
  #pendingMainTargetPromise?: (r: number) => void;
47
46
 
48
- private constructor(settings: Common.Settings.Settings, targetManager: TargetManager) {
47
+ private constructor() {
49
48
  super();
50
- this.#targetManager = targetManager;
51
49
  this.#cpuThrottlingOption = NoThrottlingOption;
52
- this.#calibratedThrottlingSetting = settings.createSetting<CalibratedCPUThrottling>(
50
+ this.#calibratedThrottlingSetting = Common.Settings.Settings.instance().createSetting<CalibratedCPUThrottling>(
53
51
  'calibrated-cpu-throttling', {}, Common.Settings.SettingStorageType.GLOBAL);
54
52
  this.#calibratedThrottlingSetting.addChangeListener(this.#onCalibratedSettingChanged, this);
55
- targetManager.observeModels(EmulationModel, this);
53
+ TargetManager.instance().observeModels(EmulationModel, this);
56
54
  }
57
55
 
58
56
  static instance(opts: {forceNew: boolean|null} = {forceNew: null}): CPUThrottlingManager {
59
57
  const {forceNew} = opts;
60
58
  if (!throttlingManagerInstance || forceNew) {
61
- throttlingManagerInstance =
62
- new CPUThrottlingManager(Common.Settings.Settings.instance(), TargetManager.instance());
59
+ throttlingManagerInstance = new CPUThrottlingManager();
63
60
  }
64
61
 
65
62
  return throttlingManagerInstance;
@@ -87,7 +84,7 @@ export class CPUThrottlingManager extends Common.ObjectWrapper.ObjectWrapper<Eve
87
84
  return;
88
85
  }
89
86
 
90
- for (const emulationModel of this.#targetManager.models(EmulationModel)) {
87
+ for (const emulationModel of TargetManager.instance().models(EmulationModel)) {
91
88
  void emulationModel.setCPUThrottlingRate(rate);
92
89
  }
93
90
  this.dispatchEventToListeners(Events.RATE_CHANGED, rate);
@@ -99,7 +96,7 @@ export class CPUThrottlingManager extends Common.ObjectWrapper.ObjectWrapper<Eve
99
96
  }
100
97
 
101
98
  this.#cpuThrottlingOption = option;
102
- for (const emulationModel of this.#targetManager.models(EmulationModel)) {
99
+ for (const emulationModel of TargetManager.instance().models(EmulationModel)) {
103
100
  void emulationModel.setCPUThrottlingRate(this.#cpuThrottlingOption.rate());
104
101
  }
105
102
  this.dispatchEventToListeners(Events.RATE_CHANGED, this.#cpuThrottlingOption.rate());
@@ -107,7 +104,7 @@ export class CPUThrottlingManager extends Common.ObjectWrapper.ObjectWrapper<Eve
107
104
 
108
105
  setHardwareConcurrency(concurrency: number): void {
109
106
  this.#hardwareConcurrency = concurrency;
110
- for (const emulationModel of this.#targetManager.models(EmulationModel)) {
107
+ for (const emulationModel of TargetManager.instance().models(EmulationModel)) {
111
108
  void emulationModel.setHardwareConcurrency(concurrency);
112
109
  }
113
110
  this.dispatchEventToListeners(Events.HARDWARE_CONCURRENCY_CHANGED, this.#hardwareConcurrency);
@@ -118,14 +115,14 @@ export class CPUThrottlingManager extends Common.ObjectWrapper.ObjectWrapper<Eve
118
115
  // target may error. So if we get any errors here at all, assume that we do
119
116
  // not have a target.
120
117
  try {
121
- return this.#targetManager.primaryPageTarget() !== null;
118
+ return TargetManager.instance().primaryPageTarget() !== null;
122
119
  } catch {
123
120
  return false;
124
121
  }
125
122
  }
126
123
 
127
124
  async getHardwareConcurrency(): Promise<number> {
128
- const target = this.#targetManager.primaryPageTarget();
125
+ const target = TargetManager.instance().primaryPageTarget();
129
126
  const existingCallback = this.#pendingMainTargetPromise;
130
127
 
131
128
  // If the main target hasn't attached yet, block callers until it appears.
@@ -334,6 +334,27 @@ export class PageResourceLoader extends Common.ObjectWrapper.ObjectWrapper<Event
334
334
  initiator.target;
335
335
  Host.userMetrics.developerResourceScheme(this.getDeveloperResourceScheme(parsedURL));
336
336
  if (eligibleForLoadFromTarget) {
337
+ let mustEnforceCSP = false;
338
+ const isHttp = parsedURL.scheme === 'http' || parsedURL.scheme === 'https';
339
+ if (isHttp && initiator.target) {
340
+ const networkManager = initiator.target.model(NetworkManager);
341
+ if (networkManager) {
342
+ let status = await networkManager.getSecurityIsolationStatus(initiator.frameId);
343
+ if (!status && initiator.frameId) {
344
+ status = await networkManager.getSecurityIsolationStatus(null);
345
+ }
346
+ if (status?.csp) {
347
+ for (const csp of status.csp) {
348
+ const directives = csp.effectiveDirectives;
349
+ if (directives.includes('connect-src') || directives.includes('default-src')) {
350
+ mustEnforceCSP = true;
351
+ break;
352
+ }
353
+ }
354
+ }
355
+ }
356
+ }
357
+
337
358
  try {
338
359
  Host.userMetrics.developerResourceLoaded(Host.UserMetrics.DeveloperResourceLoaded.LOAD_THROUGH_PAGE_VIA_TARGET);
339
360
  const result = await this.loadFromTarget(initiator.target, initiator.frameId, url, isBinary);
@@ -341,7 +362,7 @@ export class PageResourceLoader extends Common.ObjectWrapper.ObjectWrapper<Event
341
362
  } catch (e) {
342
363
  if (e instanceof Error) {
343
364
  Host.userMetrics.developerResourceLoaded(Host.UserMetrics.DeveloperResourceLoaded.LOAD_THROUGH_PAGE_FAILURE);
344
- if (e.message.includes('CSP violation')) {
365
+ if (mustEnforceCSP || e.message.includes('CSP violation')) {
345
366
  return {
346
367
  success: false,
347
368
  content: '',
@@ -444,7 +444,8 @@
444
444
  TimelineNavigationSettingState: 'DevTools.TimelineNavigationSettingState',
445
445
  SyncSetting: 'DevTools.SyncSetting',
446
446
  SwatchActivated: 'DevTools.SwatchActivated',
447
- BuiltInAiAvailability: 'DevTools.BuiltInAiAvailability'
447
+ BuiltInAiAvailability: 'DevTools.BuiltInAiAvailability',
448
+ ExtensionEvalTarget: 'DevTools.ExtensionEvalTarget'
448
449
  // LINT.ThenChange(/front_end/core/host/InspectorFrontendHostAPI.ts:EnumeratedHistogram)
449
450
  };
450
451
 
@@ -162,6 +162,11 @@ export class AiConversation {
162
162
  return this.#contexts.at(0);
163
163
  }
164
164
 
165
+ getPendingMultimodalInput(): MultimodalInput|undefined {
166
+ const greenDevEmulationEnabled = Greendev.Prototypes.instance().isEnabled('emulationCapabilities');
167
+ return greenDevEmulationEnabled ? this.#agent.popPendingMultimodalInput() : undefined;
168
+ }
169
+
165
170
  #reconstructHistory(historyWithoutImages: ResponseData[]): ResponseData[] {
166
171
  const imageHistory = AiHistoryStorage.instance().getImageHistory();
167
172
  if (imageHistory && imageHistory.length > 0) {
@@ -394,6 +394,10 @@ export abstract class AiAgent<T> {
394
394
  this.#facts.clear();
395
395
  }
396
396
 
397
+ popPendingMultimodalInput(): MultimodalInput|undefined {
398
+ return undefined;
399
+ }
400
+
397
401
  preambleFeatures(): string[] {
398
402
  return [];
399
403
  }
@@ -121,6 +121,30 @@ Content:
121
121
  "title"
122
122
  ]
123
123
  }
124
+ },
125
+ {
126
+ "name": "activateDeviceEmulation",
127
+ "description": "Sets emulation viewing mode for a specific device and optionally enables vision deficiency emulation.",
128
+ "parameters": {
129
+ "type": 6,
130
+ "description": "",
131
+ "nullable": false,
132
+ "properties": {
133
+ "deviceName": {
134
+ "type": 1,
135
+ "description": "The name of the device to emulate. Allowed values: Pixel 3 XL, Pixel 7, Samsung Galaxy S8+, Samsung Galaxy S20 Ultra, Surface Pro 7, Surface Duo, Galaxy Z Fold 5, Asus Zenbook Fold, Samsung Galaxy A51/71, Nest Hub Max, Nest Hub, iPhone 4, iPhone 5/SE, iPhone 6/7/8, iPhone SE, iPhone XR, iPhone 12 Pro, iPhone 14 Pro Max, iPad Mini, iPad Air, iPad Pro.",
136
+ "nullable": false
137
+ },
138
+ "visionDeficiency": {
139
+ "type": 1,
140
+ "description": "Optional vision deficiency to emulate. Allowed values: blurredVision, reducedContrast, achromatopsia, deuteranopia, protanopia, tritanopia.",
141
+ "nullable": true
142
+ }
143
+ },
144
+ "required": [
145
+ "deviceName"
146
+ ]
147
+ }
124
148
  }
125
149
  ],
126
150
  "options": {