chrome-devtools-frontend 1.0.1642845 → 1.0.1643099
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.
- package/SECURITY.md +1 -0
- package/front_end/core/host/UserMetrics.ts +2 -1
- package/front_end/core/protocol_client/InspectorBackend.ts +4 -0
- package/front_end/core/sdk/CSSMatchedStyles.ts +55 -26
- package/front_end/core/sdk/CSSRule.ts +1 -0
- package/front_end/core/sdk/DebuggerModel.ts +5 -0
- package/front_end/entrypoints/greendev_floaty/FloatyEntrypoint.ts +4 -3
- package/front_end/entrypoints/greendev_floaty/greendev_floaty.ts +4 -3
- package/front_end/entrypoints/heap_snapshot_worker/HeapSnapshot.ts +4 -5
- package/front_end/generated/InspectorBackendCommands.ts +1 -1
- package/front_end/generated/protocol.ts +7 -0
- package/front_end/models/ai_assistance/AiAgent2.ts +100 -18
- package/front_end/models/ai_assistance/AiConversation.ts +18 -14
- package/front_end/models/ai_assistance/AiUtils.ts +71 -0
- package/front_end/models/ai_assistance/ChangeManager.ts +2 -5
- package/front_end/models/ai_assistance/{agents/ConversationSummaryAgent.ts → ConversationSummary.ts} +29 -66
- package/front_end/models/ai_assistance/ExtensionScope.ts +1 -4
- package/front_end/models/ai_assistance/{agents/PerformanceAnnotationsAgent.ts → PerformanceAnnotations.ts} +47 -89
- package/front_end/models/ai_assistance/README.md +8 -0
- package/front_end/models/ai_assistance/agents/AccessibilityAgent.ts +65 -40
- package/front_end/models/ai_assistance/agents/AiAgent.ts +37 -6
- package/front_end/models/ai_assistance/agents/ContextSelectionAgent.snapshot.txt +11 -0
- package/front_end/models/ai_assistance/agents/ContextSelectionAgent.ts +55 -5
- package/front_end/models/ai_assistance/agents/ExecuteJavascript.ts +2 -0
- package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +119 -78
- package/front_end/models/ai_assistance/agents/StorageAgent.ts +47 -38
- package/front_end/models/ai_assistance/agents/StylingAgent.snapshot.txt +0 -25
- package/front_end/models/ai_assistance/agents/StylingAgent.ts +46 -326
- package/front_end/models/ai_assistance/ai_assistance.ts +14 -4
- package/front_end/models/ai_assistance/contexts/DOMNodeContext.snapshot.txt +51 -0
- package/front_end/models/ai_assistance/contexts/DOMNodeContext.ts +200 -0
- package/front_end/models/ai_assistance/skills/styling.md +44 -2
- package/front_end/models/ai_assistance/tools/ExecuteJavaScript.ts +140 -0
- package/front_end/models/ai_assistance/tools/GetStyles.ts +141 -0
- package/front_end/models/ai_assistance/tools/Tool.ts +64 -0
- package/front_end/models/ai_assistance/tools/ToolRegistry.ts +36 -0
- package/front_end/models/heap_snapshot/HeapSnapshotProxy.ts +5 -7
- package/front_end/models/lighthouse/LighthouseReporterTypes.ts +5 -0
- package/front_end/models/live-metrics/LiveMetrics.ts +24 -13
- package/front_end/models/stack_trace/DetailedErrorStackParser.ts +2 -2
- package/front_end/models/stack_trace/StackTrace.ts +4 -1
- package/front_end/models/stack_trace/StackTraceImpl.ts +9 -2
- package/front_end/models/stack_trace/StackTraceModel.ts +17 -4
- package/front_end/models/stack_trace/Trie.ts +1 -1
- package/front_end/panels/ai_assistance/AiAssistancePanel.ts +25 -22
- package/front_end/panels/ai_assistance/ai_assistance-meta.ts +16 -0
- package/front_end/panels/ai_assistance/components/ChatInput.ts +2 -2
- package/front_end/panels/ai_assistance/components/ChatMessage.ts +96 -4
- package/front_end/panels/ai_assistance/components/chatMessage.css +6 -0
- package/front_end/panels/application/DOMStorageItemsView.ts +4 -0
- package/front_end/panels/application/KeyValueStorageItemsView.ts +39 -7
- package/front_end/panels/application/components/AdsView.ts +219 -0
- package/front_end/panels/application/components/adsView.css +54 -0
- package/front_end/panels/application/components/components.ts +2 -0
- package/front_end/panels/common/ExtensionServer.ts +26 -15
- package/front_end/panels/console/SymbolizedErrorWidget.ts +73 -22
- package/front_end/panels/elements/StandaloneStylesContainer.ts +1 -1
- package/front_end/panels/elements/StylePropertiesSection.ts +8 -0
- package/front_end/panels/elements/StylePropertyHighlighter.ts +4 -2
- package/front_end/panels/elements/StylePropertyTreeElement.ts +6 -5
- package/front_end/panels/elements/StylesContainer.ts +1 -1
- package/front_end/panels/elements/StylesSidebarPane.ts +4 -4
- package/front_end/panels/layer_viewer/PaintProfilerView.ts +106 -132
- package/front_end/panels/lighthouse/LighthousePanel.ts +4 -3
- package/front_end/panels/network/NetworkLogView.ts +8 -1
- package/front_end/panels/network/networkLogView.css +0 -15
- package/front_end/panels/timeline/overlays/components/EntryLabelOverlay.ts +5 -4
- package/front_end/third_party/chromium/README.chromium +1 -1
- package/front_end/third_party/lighthouse/README.chromium +2 -2
- package/front_end/third_party/lighthouse/lighthouse-dt-bundle.js +1607 -5733
- package/front_end/third_party/lighthouse/locales/ar-XB.json +290 -65
- package/front_end/third_party/lighthouse/locales/ar.json +290 -65
- package/front_end/third_party/lighthouse/locales/bg.json +290 -65
- package/front_end/third_party/lighthouse/locales/ca.json +295 -70
- package/front_end/third_party/lighthouse/locales/cs.json +290 -65
- package/front_end/third_party/lighthouse/locales/da.json +294 -69
- package/front_end/third_party/lighthouse/locales/de.json +295 -70
- package/front_end/third_party/lighthouse/locales/el.json +290 -65
- package/front_end/third_party/lighthouse/locales/en-GB.json +290 -65
- package/front_end/third_party/lighthouse/locales/en-US.json +79 -67
- package/front_end/third_party/lighthouse/locales/en-XA.json +253 -64
- package/front_end/third_party/lighthouse/locales/en-XL.json +79 -67
- package/front_end/third_party/lighthouse/locales/es-419.json +290 -65
- package/front_end/third_party/lighthouse/locales/es.json +298 -73
- package/front_end/third_party/lighthouse/locales/fi.json +290 -65
- package/front_end/third_party/lighthouse/locales/fil.json +290 -65
- package/front_end/third_party/lighthouse/locales/fr.json +294 -69
- package/front_end/third_party/lighthouse/locales/he.json +293 -68
- package/front_end/third_party/lighthouse/locales/hi.json +291 -66
- package/front_end/third_party/lighthouse/locales/hr.json +290 -65
- package/front_end/third_party/lighthouse/locales/hu.json +290 -65
- package/front_end/third_party/lighthouse/locales/id.json +290 -65
- package/front_end/third_party/lighthouse/locales/it.json +294 -69
- package/front_end/third_party/lighthouse/locales/ja.json +290 -65
- package/front_end/third_party/lighthouse/locales/ko.json +290 -65
- package/front_end/third_party/lighthouse/locales/lt.json +290 -65
- package/front_end/third_party/lighthouse/locales/lv.json +290 -65
- package/front_end/third_party/lighthouse/locales/nl.json +290 -65
- package/front_end/third_party/lighthouse/locales/no.json +290 -65
- package/front_end/third_party/lighthouse/locales/pl.json +290 -65
- package/front_end/third_party/lighthouse/locales/pt-PT.json +291 -66
- package/front_end/third_party/lighthouse/locales/pt.json +290 -65
- package/front_end/third_party/lighthouse/locales/ro.json +290 -65
- package/front_end/third_party/lighthouse/locales/ru.json +301 -76
- package/front_end/third_party/lighthouse/locales/sk.json +291 -66
- package/front_end/third_party/lighthouse/locales/sl.json +290 -65
- package/front_end/third_party/lighthouse/locales/sr-Latn.json +290 -65
- package/front_end/third_party/lighthouse/locales/sr.json +290 -65
- package/front_end/third_party/lighthouse/locales/sv.json +297 -72
- package/front_end/third_party/lighthouse/locales/ta.json +291 -66
- package/front_end/third_party/lighthouse/locales/te.json +293 -68
- package/front_end/third_party/lighthouse/locales/th.json +291 -66
- package/front_end/third_party/lighthouse/locales/tr.json +290 -65
- package/front_end/third_party/lighthouse/locales/uk.json +290 -65
- package/front_end/third_party/lighthouse/locales/vi.json +291 -66
- package/front_end/third_party/lighthouse/locales/zh-HK.json +292 -67
- package/front_end/third_party/lighthouse/locales/zh-TW.json +291 -66
- package/front_end/third_party/lighthouse/locales/zh.json +291 -66
- package/front_end/third_party/lighthouse/report/bundle.d.ts +6 -6
- package/front_end/third_party/lighthouse/report/bundle.js +4 -7
- package/front_end/third_party/lighthouse/report-assets/report-generator.mjs +2 -2
- package/front_end/ui/legacy/Widget.ts +32 -8
- package/front_end/ui/legacy/components/cookie_table/CookiesTable.ts +36 -3
- package/front_end/ui/legacy/components/data_grid/dataGridAiButton.css +20 -0
- package/front_end/ui/legacy/components/utils/Linkifier.ts +19 -4
- package/front_end/ui/visual_logging/KnownContextValues.ts +3 -0
- package/mcp/mcp.ts +1 -0
- package/package.json +1 -1
|
@@ -28,8 +28,13 @@
|
|
|
28
28
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
29
29
|
*/
|
|
30
30
|
/* eslint no-return-assign: "off" */
|
|
31
|
+
import '../../ui/components/buttons/buttons.js';
|
|
32
|
+
|
|
31
33
|
import * as i18n from '../../core/i18n/i18n.js';
|
|
34
|
+
import * as AIAssistance from '../../models/ai_assistance/ai_assistance.js';
|
|
32
35
|
import * as Geometry from '../../models/geometry/geometry.js';
|
|
36
|
+
// eslint-disable-next-line @devtools/es-modules-import
|
|
37
|
+
import dataGridAiButtonStyles from '../../ui/legacy/components/data_grid/dataGridAiButton.css.js';
|
|
33
38
|
import * as UI from '../../ui/legacy/legacy.js';
|
|
34
39
|
import {Directives as LitDirectives, html, nothing, render} from '../../ui/lit/lit.js';
|
|
35
40
|
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
|
|
@@ -37,11 +42,13 @@ import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
|
|
|
37
42
|
import * as ApplicationComponents from './components/components.js';
|
|
38
43
|
import {StorageItemsToolbar} from './StorageItemsToolbar.js';
|
|
39
44
|
|
|
45
|
+
const STORAGE_FLOATING_BUTTON_ACTION_ID = 'ai-assistance.storage-floating-button';
|
|
46
|
+
|
|
40
47
|
const {ARIAUtils} = UI;
|
|
41
48
|
const {EmptyWidget} = UI.EmptyWidget;
|
|
42
49
|
const {VBox, widget} = UI.Widget;
|
|
43
50
|
const {Size} = Geometry;
|
|
44
|
-
const {repeat} = LitDirectives;
|
|
51
|
+
const {repeat, ifDefined} = LitDirectives;
|
|
45
52
|
|
|
46
53
|
type Widget = UI.Widget.Widget;
|
|
47
54
|
type VBox = UI.Widget.VBox;
|
|
@@ -87,6 +94,9 @@ export interface ViewInput {
|
|
|
87
94
|
onDeleteAll: () => void;
|
|
88
95
|
jslog?: string;
|
|
89
96
|
classes?: string[];
|
|
97
|
+
aiButtonTitle?: string;
|
|
98
|
+
showAiButton?: boolean;
|
|
99
|
+
onAiButtonClick?: (item: {key: string, value: string}, event: Event) => void;
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
interface ViewOutput {
|
|
@@ -150,6 +160,7 @@ export abstract class KeyValueStorageItemsView extends UI.Widget.VBox {
|
|
|
150
160
|
@deselect=${() => input.onSelect(null)}
|
|
151
161
|
>
|
|
152
162
|
<table>
|
|
163
|
+
${input.showAiButton ? html`<style>${dataGridAiButtonStyles}</style>`: nothing}
|
|
153
164
|
<tr>
|
|
154
165
|
<th id="key" sortable ?editable=${input.editable}>
|
|
155
166
|
${i18nString(UIStrings.key)}
|
|
@@ -165,7 +176,15 @@ export abstract class KeyValueStorageItemsView extends UI.Widget.VBox {
|
|
|
165
176
|
input.onEdit(item.key, item.value, e.detail.columnId, e.detail.valueBeforeEditing, e.detail.newText)}
|
|
166
177
|
@delete=${() => input.onDelete(item.key)}
|
|
167
178
|
selected=${(input.selectedKey === item.key) || nothing}>
|
|
168
|
-
<td>${
|
|
179
|
+
<td>${input.showAiButton ? html`
|
|
180
|
+
<span class="ai-button-container">
|
|
181
|
+
<devtools-floating-button
|
|
182
|
+
icon-name=${AIAssistance.AiUtils.getIconName()}
|
|
183
|
+
title=${ifDefined(input.aiButtonTitle)}
|
|
184
|
+
@click=${(e: Event) => input.onAiButtonClick?.(item, e)}
|
|
185
|
+
></devtools-floating-button>
|
|
186
|
+
</span>
|
|
187
|
+
` : nothing}${item.key}</td>
|
|
169
188
|
<td>${item.value.substr(0, MAX_VALUE_LENGTH)}</td>
|
|
170
189
|
</tr>`)}
|
|
171
190
|
<tr placeholder></tr>
|
|
@@ -217,15 +236,24 @@ export abstract class KeyValueStorageItemsView extends UI.Widget.VBox {
|
|
|
217
236
|
preview: this.#preview,
|
|
218
237
|
jslog: this.#jslog,
|
|
219
238
|
classes: this.#classes,
|
|
239
|
+
showAiButton: this.isAiButtonEnabled(),
|
|
240
|
+
aiButtonTitle: this.isAiButtonEnabled() &&
|
|
241
|
+
UI.ActionRegistry.ActionRegistry.instance().hasAction(STORAGE_FLOATING_BUTTON_ACTION_ID) ?
|
|
242
|
+
UI.ActionRegistry.ActionRegistry.instance().getAction(STORAGE_FLOATING_BUTTON_ACTION_ID).title() :
|
|
243
|
+
undefined,
|
|
220
244
|
onSelect: (item: {key: string, value: string}|null) => {
|
|
221
245
|
this.#toolbar?.setCanDeleteSelected(Boolean(item));
|
|
222
|
-
|
|
223
|
-
void this.#previewEntry(null);
|
|
224
|
-
} else {
|
|
225
|
-
void this.#previewEntry(item);
|
|
226
|
-
}
|
|
246
|
+
void this.#previewEntry(item);
|
|
227
247
|
this.selectedItemChanged(item);
|
|
228
248
|
},
|
|
249
|
+
onAiButtonClick: (item: {key: string, value: string}, event: Event) => {
|
|
250
|
+
event.stopPropagation();
|
|
251
|
+
viewInput.onSelect(item);
|
|
252
|
+
const actionRegistry = UI.ActionRegistry.ActionRegistry.instance();
|
|
253
|
+
if (actionRegistry.hasAction(STORAGE_FLOATING_BUTTON_ACTION_ID)) {
|
|
254
|
+
void actionRegistry.getAction(STORAGE_FLOATING_BUTTON_ACTION_ID).execute();
|
|
255
|
+
}
|
|
256
|
+
},
|
|
229
257
|
|
|
230
258
|
onSort: (ascending: boolean) => {
|
|
231
259
|
this.#isSortOrderAscending = ascending;
|
|
@@ -252,6 +280,10 @@ export abstract class KeyValueStorageItemsView extends UI.Widget.VBox {
|
|
|
252
280
|
this.#view(viewInput, viewOutput, this.contentElement);
|
|
253
281
|
}
|
|
254
282
|
|
|
283
|
+
protected isAiButtonEnabled(): boolean {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
255
287
|
protected get toolbar(): StorageItemsToolbar|undefined {
|
|
256
288
|
return this.#toolbar;
|
|
257
289
|
}
|
|
@@ -0,0 +1,219 @@
|
|
|
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 i18n from '../../../core/i18n/i18n.js';
|
|
6
|
+
import * as SDK from '../../../core/sdk/sdk.js';
|
|
7
|
+
import type * as Protocol from '../../../generated/protocol.js';
|
|
8
|
+
import * as UI from '../../../ui/legacy/legacy.js';
|
|
9
|
+
import * as Lit from '../../../ui/lit/lit.js';
|
|
10
|
+
|
|
11
|
+
import adsViewStyles from './adsView.css.js';
|
|
12
|
+
|
|
13
|
+
const {html} = Lit;
|
|
14
|
+
|
|
15
|
+
const UIStrings = {
|
|
16
|
+
/**
|
|
17
|
+
* @description Title for a metric showing the percentage of the viewport covered by ads.
|
|
18
|
+
*/
|
|
19
|
+
viewportAdDensity: 'Viewport ad density',
|
|
20
|
+
/**
|
|
21
|
+
* @description Title for a metric showing the number of ads in the viewport.
|
|
22
|
+
*/
|
|
23
|
+
viewportAdCount: 'Viewport ad count',
|
|
24
|
+
/**
|
|
25
|
+
* @description Title for a metric showing the total CPU usage by ads.
|
|
26
|
+
*/
|
|
27
|
+
totalCpuUsage: 'Total CPU usage by ads',
|
|
28
|
+
/**
|
|
29
|
+
* @description Title for a metric showing the total network usage by ads.
|
|
30
|
+
*/
|
|
31
|
+
totalNetworkUsage: 'Total network usage by ads',
|
|
32
|
+
/**
|
|
33
|
+
* @description Subtext showing the average value of a metric.
|
|
34
|
+
* @example {5.00%} PH1
|
|
35
|
+
*/
|
|
36
|
+
average: '(Average: {PH1})',
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
const str_ = i18n.i18n.registerUIStrings('panels/application/components/AdsView.ts', UIStrings);
|
|
40
|
+
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
|
41
|
+
|
|
42
|
+
export interface ViewInput {
|
|
43
|
+
metrics: Protocol.Ads.AdMetrics;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type View = (input: ViewInput, output: undefined, target: HTMLElement|DocumentFragment) => void;
|
|
47
|
+
|
|
48
|
+
const DEFAULT_VIEW: View = (input, output, target) => {
|
|
49
|
+
const metrics = input.metrics;
|
|
50
|
+
|
|
51
|
+
const formatValue = (val: number, isPercentage: boolean): string => {
|
|
52
|
+
if (isPercentage) {
|
|
53
|
+
return new Intl
|
|
54
|
+
.NumberFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale, {
|
|
55
|
+
style: 'percent',
|
|
56
|
+
maximumFractionDigits: 0,
|
|
57
|
+
})
|
|
58
|
+
.format(val / 100);
|
|
59
|
+
}
|
|
60
|
+
return new Intl.NumberFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale).format(val);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const formatAverage = (val: number, isPercentage: boolean): string => {
|
|
64
|
+
if (isPercentage) {
|
|
65
|
+
return new Intl
|
|
66
|
+
.NumberFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale, {
|
|
67
|
+
style: 'percent',
|
|
68
|
+
minimumFractionDigits: 2,
|
|
69
|
+
maximumFractionDigits: 2,
|
|
70
|
+
})
|
|
71
|
+
.format(val / 100);
|
|
72
|
+
}
|
|
73
|
+
return new Intl
|
|
74
|
+
.NumberFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale, {
|
|
75
|
+
minimumFractionDigits: 2,
|
|
76
|
+
maximumFractionDigits: 2,
|
|
77
|
+
})
|
|
78
|
+
.format(val);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const formatCpu = (val: number): string => {
|
|
82
|
+
return i18n.TimeUtilities.preciseMillisToString(val, 1);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const formatNetwork = (val: number): string => {
|
|
86
|
+
return i18n.ByteUtilities.bytesToString(val);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// clang-format off
|
|
90
|
+
Lit.render(html`
|
|
91
|
+
<style>${adsViewStyles}</style>
|
|
92
|
+
<dl class="metrics-container">
|
|
93
|
+
<div class="metric-box">
|
|
94
|
+
<dt class="metric-title">${i18nString(UIStrings.viewportAdDensity)}</dt>
|
|
95
|
+
<dd class="metric-value">
|
|
96
|
+
<span>${formatValue(metrics.viewportAdDensityByArea, true)}</span>
|
|
97
|
+
<span class="metric-average">${i18nString(UIStrings.average, {PH1: formatAverage(metrics.averageViewportAdDensityByArea, true)})}</span>
|
|
98
|
+
</dd>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="metric-box">
|
|
101
|
+
<dt class="metric-title">${i18nString(UIStrings.viewportAdCount)}</dt>
|
|
102
|
+
<dd class="metric-value">
|
|
103
|
+
<span>${formatValue(metrics.viewportAdCount, false)}</span>
|
|
104
|
+
<span class="metric-average">${i18nString(UIStrings.average, {PH1: formatAverage(metrics.averageViewportAdCount, false)})}</span>
|
|
105
|
+
</dd>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="metric-box">
|
|
108
|
+
<dt class="metric-title">${i18nString(UIStrings.totalCpuUsage)}</dt>
|
|
109
|
+
<dd class="metric-value">
|
|
110
|
+
<span>${formatCpu(metrics.totalAdCpuTime)}</span>
|
|
111
|
+
</dd>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="metric-box">
|
|
114
|
+
<dt class="metric-title">${i18nString(UIStrings.totalNetworkUsage)}</dt>
|
|
115
|
+
<dd class="metric-value">
|
|
116
|
+
<span>${formatNetwork(metrics.totalAdNetworkBytes)}</span>
|
|
117
|
+
</dd>
|
|
118
|
+
</div>
|
|
119
|
+
</dl>
|
|
120
|
+
`, target);
|
|
121
|
+
// clang-format on
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export class AdsView extends UI.Widget.Widget {
|
|
125
|
+
#currentMetrics: Protocol.Ads.AdMetrics;
|
|
126
|
+
#pollTimer?: number;
|
|
127
|
+
#isPolling = false;
|
|
128
|
+
#pollSessionId = 0;
|
|
129
|
+
#view: View;
|
|
130
|
+
|
|
131
|
+
constructor(view: View = DEFAULT_VIEW) {
|
|
132
|
+
super({useShadowDom: true});
|
|
133
|
+
this.#view = view;
|
|
134
|
+
this.#currentMetrics = {
|
|
135
|
+
viewportAdDensityByArea: 0,
|
|
136
|
+
averageViewportAdDensityByArea: 0,
|
|
137
|
+
viewportAdCount: 0,
|
|
138
|
+
averageViewportAdCount: 0,
|
|
139
|
+
totalAdCpuTime: 0,
|
|
140
|
+
totalAdNetworkBytes: 0,
|
|
141
|
+
};
|
|
142
|
+
this.requestUpdate();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
override wasShown(): void {
|
|
146
|
+
super.wasShown();
|
|
147
|
+
this.#startPolling();
|
|
148
|
+
SDK.TargetManager.TargetManager.instance().addModelListener(SDK.ResourceTreeModel.ResourceTreeModel,
|
|
149
|
+
SDK.ResourceTreeModel.Events.PrimaryPageChanged,
|
|
150
|
+
this.#onPrimaryPageChanged, this);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
override willHide(): void {
|
|
154
|
+
this.#stopPolling();
|
|
155
|
+
SDK.TargetManager.TargetManager.instance().removeModelListener(SDK.ResourceTreeModel.ResourceTreeModel,
|
|
156
|
+
SDK.ResourceTreeModel.Events.PrimaryPageChanged,
|
|
157
|
+
this.#onPrimaryPageChanged, this);
|
|
158
|
+
super.willHide();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#startPolling(): void {
|
|
162
|
+
if (this.#isPolling) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
this.#isPolling = true;
|
|
166
|
+
this.#pollSessionId++;
|
|
167
|
+
void this.#pollMetrics(this.#pollSessionId);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#stopPolling(): void {
|
|
171
|
+
this.#isPolling = false;
|
|
172
|
+
if (this.#pollTimer !== undefined) {
|
|
173
|
+
window.clearTimeout(this.#pollTimer);
|
|
174
|
+
this.#pollTimer = undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async #pollMetrics(sessionId: number): Promise<void> {
|
|
179
|
+
if (!this.#isPolling || this.#pollSessionId !== sessionId) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
|
|
183
|
+
if (target) {
|
|
184
|
+
const adsAgent = target.adsAgent();
|
|
185
|
+
if (adsAgent) {
|
|
186
|
+
const response = await adsAgent.invoke_getAdMetrics();
|
|
187
|
+
if (!this.#isPolling || this.#pollSessionId !== sessionId) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (!response.getError()) {
|
|
191
|
+
this.#currentMetrics = response.metrics;
|
|
192
|
+
this.requestUpdate();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (this.#isPolling && this.#pollSessionId === sessionId) {
|
|
197
|
+
this.#pollTimer = window.setTimeout(() => this.#pollMetrics(sessionId), 500);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#onPrimaryPageChanged(): void {
|
|
202
|
+
this.#currentMetrics = {
|
|
203
|
+
viewportAdDensityByArea: 0,
|
|
204
|
+
averageViewportAdDensityByArea: 0,
|
|
205
|
+
viewportAdCount: 0,
|
|
206
|
+
averageViewportAdCount: 0,
|
|
207
|
+
totalAdCpuTime: 0,
|
|
208
|
+
totalAdNetworkBytes: 0,
|
|
209
|
+
};
|
|
210
|
+
this.requestUpdate();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
override performUpdate(): void {
|
|
214
|
+
const viewInput: ViewInput = {
|
|
215
|
+
metrics: this.#currentMetrics,
|
|
216
|
+
};
|
|
217
|
+
this.#view(viewInput, undefined, this.contentElement);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 The Chromium Authors
|
|
3
|
+
* Use of this source code is governed by a BSD-style license that can be
|
|
4
|
+
* found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
:host {
|
|
8
|
+
padding: 12px;
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
overflow: auto;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.metrics-container {
|
|
15
|
+
flex: 0 0 auto;
|
|
16
|
+
margin: 0 0 24px;
|
|
17
|
+
border: 1px solid var(--sys-color-divider);
|
|
18
|
+
display: grid;
|
|
19
|
+
grid-template-columns: repeat(2, 1fr);
|
|
20
|
+
gap: 1px;
|
|
21
|
+
background-color: var(--sys-color-divider);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.metric-box {
|
|
25
|
+
background-color: var(--sys-color-surface);
|
|
26
|
+
padding: 12px;
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.metric-title {
|
|
34
|
+
font-size: 12px;
|
|
35
|
+
color: var(--sys-color-on-surface-subtle);
|
|
36
|
+
margin: 0 0 4px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.metric-value {
|
|
40
|
+
font-size: 18px;
|
|
41
|
+
font-weight: bold;
|
|
42
|
+
color: var(--sys-color-on-surface);
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
align-items: center;
|
|
46
|
+
margin: 0;
|
|
47
|
+
gap: 2px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.metric-average {
|
|
51
|
+
font-size: 12px;
|
|
52
|
+
font-weight: normal;
|
|
53
|
+
color: var(--sys-color-on-surface-subtle);
|
|
54
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Use of this source code is governed by a BSD-style license that can be
|
|
3
3
|
// found in the LICENSE file.
|
|
4
4
|
|
|
5
|
+
import * as AdsView from './AdsView.js';
|
|
5
6
|
import * as BackForwardCacheView from './BackForwardCacheView.js';
|
|
6
7
|
import * as BounceTrackingMitigationsView from './BounceTrackingMitigationsView.js';
|
|
7
8
|
import * as CrashReportContextGrid from './CrashReportContextGrid.js';
|
|
@@ -17,6 +18,7 @@ import * as StorageMetadataView from './StorageMetadataView.js';
|
|
|
17
18
|
import * as TrustTokensView from './TrustTokensView.js';
|
|
18
19
|
|
|
19
20
|
export {
|
|
21
|
+
AdsView,
|
|
20
22
|
BackForwardCacheView,
|
|
21
23
|
BounceTrackingMitigationsView,
|
|
22
24
|
CrashReportContextGrid,
|
|
@@ -27,7 +27,13 @@ import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js';
|
|
|
27
27
|
import {ExtensionButton, ExtensionPanel, ExtensionSidebarPane} from './ExtensionPanel.js';
|
|
28
28
|
|
|
29
29
|
const extensionOrigins = new WeakMap<MessagePort, Platform.DevToolsPath.UrlString>();
|
|
30
|
-
const
|
|
30
|
+
const kForbiddenSchemes = [
|
|
31
|
+
'chrome:',
|
|
32
|
+
'chrome-untrusted:',
|
|
33
|
+
'chrome-error:',
|
|
34
|
+
'chrome-search:',
|
|
35
|
+
'devtools:',
|
|
36
|
+
];
|
|
31
37
|
|
|
32
38
|
declare global {
|
|
33
39
|
interface Window {
|
|
@@ -78,7 +84,6 @@ export class HostsPolicy {
|
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
class RegisteredExtension {
|
|
81
|
-
openResourceScheme: null|string = null;
|
|
82
87
|
constructor(readonly name: string, readonly hostsPolicy: HostsPolicy, readonly allowFileAccess: boolean) {
|
|
83
88
|
}
|
|
84
89
|
|
|
@@ -91,8 +96,11 @@ class RegisteredExtension {
|
|
|
91
96
|
return false;
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
let parsedURL;
|
|
100
|
+
try {
|
|
101
|
+
parsedURL = new URL(inspectedURL);
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
if (!ExtensionServer.canInspectURL(inspectedURL)) {
|
|
@@ -104,12 +112,6 @@ class RegisteredExtension {
|
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
if (!this.allowFileAccess) {
|
|
107
|
-
let parsedURL;
|
|
108
|
-
try {
|
|
109
|
-
parsedURL = new URL(inspectedURL);
|
|
110
|
-
} catch {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
115
|
return parsedURL.protocol !== 'file:';
|
|
114
116
|
}
|
|
115
117
|
|
|
@@ -863,18 +865,27 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
|
|
|
863
865
|
if (!extension) {
|
|
864
866
|
throw new Error('Received a message from an unregistered extension');
|
|
865
867
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
+
let validScheme = message.urlScheme;
|
|
869
|
+
if (validScheme) {
|
|
870
|
+
try {
|
|
871
|
+
const urlToParse = validScheme.replace(/:?(\/\/)?$/, '') + '://test';
|
|
872
|
+
validScheme = new URL(urlToParse).protocol;
|
|
873
|
+
} catch {
|
|
874
|
+
return this.status.E_BADARG('urlScheme', 'Invalid scheme');
|
|
875
|
+
}
|
|
876
|
+
if (kForbiddenSchemes.includes(validScheme) || validScheme === 'file:') {
|
|
877
|
+
return this.status.E_BADARG('urlScheme', 'Scheme is forbidden');
|
|
878
|
+
}
|
|
868
879
|
}
|
|
869
880
|
const extensionOrigin = this.getExtensionOrigin(port);
|
|
870
881
|
const {name} = extension;
|
|
871
882
|
const registration = {
|
|
872
883
|
title: name,
|
|
873
884
|
origin: extensionOrigin,
|
|
874
|
-
scheme:
|
|
885
|
+
scheme: validScheme,
|
|
875
886
|
handler: this.handleOpenURL.bind(this, port),
|
|
876
887
|
shouldHandleOpenResource: (url: Platform.DevToolsPath.UrlString, schemes: Set<string>) =>
|
|
877
|
-
Components.Linkifier.Linkifier.shouldHandleOpenResource(
|
|
888
|
+
Components.Linkifier.Linkifier.shouldHandleOpenResource(validScheme || null, url, schemes),
|
|
878
889
|
};
|
|
879
890
|
if (message.handlerPresent) {
|
|
880
891
|
Components.Linkifier.Linkifier.registerLinkHandler(registration);
|
|
@@ -1645,7 +1656,7 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
|
|
|
1645
1656
|
return false;
|
|
1646
1657
|
}
|
|
1647
1658
|
|
|
1648
|
-
if (
|
|
1659
|
+
if (kForbiddenSchemes.includes(parsedURL.protocol)) {
|
|
1649
1660
|
return false;
|
|
1650
1661
|
}
|
|
1651
1662
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Use of this source code is governed by a BSD-style license that can be
|
|
3
3
|
// found in the LICENSE file.
|
|
4
4
|
|
|
5
|
+
import type * as Platform from '../../core/platform/platform.js';
|
|
5
6
|
import * as Bindings from '../../models/bindings/bindings.js';
|
|
6
7
|
import type * as StackTrace from '../../models/stack_trace/stack_trace.js';
|
|
7
8
|
import type * as Workspace from '../../models/workspace/workspace.js';
|
|
@@ -9,6 +10,8 @@ import * as Components from '../../ui/legacy/components/utils/utils.js';
|
|
|
9
10
|
import * as UI from '../../ui/legacy/legacy.js';
|
|
10
11
|
import * as Lit from '../../ui/lit/lit.js';
|
|
11
12
|
|
|
13
|
+
import {ConsoleViewMessage} from './ConsoleViewMessage.js';
|
|
14
|
+
|
|
12
15
|
const {html, render} = Lit;
|
|
13
16
|
|
|
14
17
|
export interface ViewInput {
|
|
@@ -24,20 +27,64 @@ function renderHeader(content: Lit.LitTemplate|Node|UI.Widget.Widget, isCause: b
|
|
|
24
27
|
return html`<span class="error-message-text">${content}</span>`;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
function formatName(frame: StackTrace.StackTrace.ParsedErrorStackFrame): string {
|
|
31
|
+
let name = frame.name || '';
|
|
32
|
+
const isInline = Boolean(frame.rawName) && frame.name !== frame.rawName;
|
|
33
|
+
const shouldAppendMethodAlias = !isInline && frame.methodName && name && name !== frame.methodName &&
|
|
34
|
+
!name.endsWith('.' + frame.methodName) && !name.endsWith(' ' + frame.methodName);
|
|
35
|
+
if (shouldAppendMethodAlias) {
|
|
36
|
+
name += ` [as ${frame.methodName}]`;
|
|
37
|
+
}
|
|
38
|
+
return name;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderLinkElement(frame: StackTrace.StackTrace.ParsedErrorStackFrame,
|
|
42
|
+
options: Components.Linkifier.LinkifyOptions): HTMLElement|Lit.LitTemplate {
|
|
43
|
+
if (frame.url || frame.uiSourceCode) {
|
|
44
|
+
const link = Components.Linkifier.Linkifier.linkifyStackTraceFrame(frame, options);
|
|
45
|
+
link.tabIndex = -1;
|
|
46
|
+
return link;
|
|
47
|
+
}
|
|
48
|
+
return html`<span><anonymous></span>`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderEvalOrigin(frame: StackTrace.StackTrace.ParsedErrorStackFrame,
|
|
52
|
+
options: Components.Linkifier.LinkifyOptions): Lit.LitTemplate {
|
|
53
|
+
const name = formatName(frame);
|
|
54
|
+
const linkElement = renderLinkElement(frame, options);
|
|
55
|
+
|
|
56
|
+
const asyncPrefix = frame.isAsync ? 'async ' : '';
|
|
57
|
+
const constructorPrefix = frame.isConstructor ? 'new ' : '';
|
|
58
|
+
|
|
59
|
+
if (frame.isEval) {
|
|
60
|
+
const evalOrigin = frame.evalOrigin ? renderEvalOrigin(frame.evalOrigin, options) : '<anonymous>';
|
|
61
|
+
if (name) {
|
|
62
|
+
return html`${asyncPrefix}${constructorPrefix}eval at ${name} (${evalOrigin})`;
|
|
63
|
+
}
|
|
64
|
+
return html`${asyncPrefix}${constructorPrefix}eval at ${evalOrigin}`;
|
|
65
|
+
}
|
|
66
|
+
if (name) {
|
|
67
|
+
return html`${asyncPrefix}${constructorPrefix}eval at ${name} (${linkElement})`;
|
|
68
|
+
}
|
|
69
|
+
return html`${asyncPrefix}${constructorPrefix}eval at ${linkElement}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function renderFramePrefix(frame: StackTrace.StackTrace.ParsedErrorStackFrame,
|
|
73
|
+
options: Components.Linkifier.LinkifyOptions): Lit.LitTemplate {
|
|
30
74
|
const asyncPrefix = frame.isAsync ? 'async ' : '';
|
|
31
75
|
if (frame.promiseIndex !== undefined) {
|
|
32
76
|
const name = frame.name || 'Promise.all';
|
|
33
77
|
return html`${asyncPrefix}${name} (index ${frame.promiseIndex})`;
|
|
34
78
|
}
|
|
35
79
|
const constructorPrefix = frame.isConstructor ? 'new ' : '';
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
name
|
|
80
|
+
const name = formatName(frame);
|
|
81
|
+
|
|
82
|
+
if (frame.isEval) {
|
|
83
|
+
const evalOrigin = frame.evalOrigin ? renderEvalOrigin(frame.evalOrigin, options) : '<anonymous>';
|
|
84
|
+
if (name) {
|
|
85
|
+
return html`${asyncPrefix}${constructorPrefix}${name} (${evalOrigin}, `;
|
|
86
|
+
}
|
|
87
|
+
return html`${asyncPrefix}${constructorPrefix}${evalOrigin}, `;
|
|
41
88
|
}
|
|
42
89
|
|
|
43
90
|
if (name) {
|
|
@@ -58,10 +105,25 @@ function renderFrameSuffix(frame: StackTrace.StackTrace.ParsedErrorStackFrame):
|
|
|
58
105
|
|
|
59
106
|
const DEFAULT_VIEW = (input: ViewInput, _output: object, target: HTMLElement): void => {
|
|
60
107
|
const renderError = (error: Bindings.SymbolizedError.SymbolizedError, isCause: boolean): Lit.LitTemplate => {
|
|
61
|
-
if (
|
|
108
|
+
if (error instanceof Bindings.SymbolizedError.SymbolizedSyntaxError) {
|
|
62
109
|
console.error('SymbolizedErrorWidget received an unsupported error type:', error);
|
|
63
110
|
return Lit.nothing;
|
|
64
111
|
}
|
|
112
|
+
if (error instanceof Bindings.SymbolizedError.UnparsableError) {
|
|
113
|
+
const fragment = ConsoleViewMessage.linkifyWithCustomLinkifier(
|
|
114
|
+
error.errorStack,
|
|
115
|
+
(text: string, url: Platform.DevToolsPath.UrlString, lineNumber?: number, columnNumber?: number) => {
|
|
116
|
+
const options = {text, lineNumber, columnNumber, ignoreListManager: input.ignoreListManager};
|
|
117
|
+
const linkElement = Components.Linkifier.Linkifier.linkifyURL(url, options);
|
|
118
|
+
linkElement.tabIndex = -1;
|
|
119
|
+
return linkElement;
|
|
120
|
+
});
|
|
121
|
+
const header = renderHeader(fragment, isCause);
|
|
122
|
+
return html`
|
|
123
|
+
<span class=${isCause ? 'console-message-stack-trace-wrapper' : ''}>${header}</span>
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
|
|
65
127
|
const linkOptions: Components.Linkifier.LinkifyOptions = {
|
|
66
128
|
showColumnNumber: true,
|
|
67
129
|
inlineFrameIndex: 0,
|
|
@@ -75,19 +137,8 @@ const DEFAULT_VIEW = (input: ViewInput, _output: object, target: HTMLElement): v
|
|
|
75
137
|
return html`
|
|
76
138
|
<span class=${isCause ? 'console-message-stack-trace-wrapper' : ''}
|
|
77
139
|
>${header}${syncFrames.length > 0 ? '\n' : ''}${syncFrames.map((frame: StackTrace.StackTrace.ParsedErrorStackFrame, i: number) => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (frame.promiseIndex !== undefined) {
|
|
81
|
-
// Promise.all doesn't have a linkable location.
|
|
82
|
-
isBuiltin = true;
|
|
83
|
-
} else if (frame.url || frame.uiSourceCode) {
|
|
84
|
-
const link = Components.Linkifier.Linkifier.linkifyStackTraceFrame(frame, linkOptions);
|
|
85
|
-
link.tabIndex = -1;
|
|
86
|
-
linkElement = link;
|
|
87
|
-
} else {
|
|
88
|
-
linkElement = html`<span><anonymous></span>`;
|
|
89
|
-
isBuiltin = true;
|
|
90
|
-
}
|
|
140
|
+
const isBuiltin = frame.promiseIndex !== undefined || (!frame.url && !frame.uiSourceCode);
|
|
141
|
+
const linkElement = frame.promiseIndex !== undefined ? Lit.nothing : renderLinkElement(frame, linkOptions);
|
|
91
142
|
|
|
92
143
|
const newline = i < error.stackTrace.syncFragment.frames.length - 1 ? '\n' : '';
|
|
93
144
|
const frameClass = isBuiltin ? 'formatted-builtin-stack-frame' : 'formatted-stack-frame';
|
|
@@ -276,7 +276,7 @@ export class StandaloneStylesContainer extends Common.ObjectWrapper.eventMixin<E
|
|
|
276
276
|
return null;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
jumpToFunctionDefinition(_functionName: string): void {
|
|
279
|
+
jumpToFunctionDefinition(_functionName: string, _treeScopeDistance: number): void {
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
continueEditingElement(_sectionIndex: number, _propertyIndex: number): void {
|
|
@@ -379,6 +379,14 @@ export class StylePropertiesSection {
|
|
|
379
379
|
return this.sectionIdx;
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
+
treeScopeDistance(): number {
|
|
383
|
+
const treeScope = this.styleInternal.parentRule?.treeScope;
|
|
384
|
+
if (!treeScope) {
|
|
385
|
+
return -1;
|
|
386
|
+
}
|
|
387
|
+
return SDK.CSSMatchedStyles.distanceToTreeScope(this.matchedStyles.node(), treeScope);
|
|
388
|
+
}
|
|
389
|
+
|
|
382
390
|
static createRuleOriginNode(
|
|
383
391
|
matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles, linkifier: Components.Linkifier.Linkifier,
|
|
384
392
|
rule: SDK.CSSRule.CSSRule|null): LitTemplate {
|
|
@@ -51,9 +51,11 @@ export class StylePropertyHighlighter {
|
|
|
51
51
|
PanelUtils.highlightElement(block.titleElement() as HTMLElement);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
findAndHighlightSection(sectionName: string, blockName: string): void {
|
|
54
|
+
findAndHighlightSection(sectionName: string, blockName: string, treeScopeDistance = -1): void {
|
|
55
55
|
const block = this.styleSidebarPane.getSectionBlockByName(blockName);
|
|
56
|
-
const section = block?.sections.find(
|
|
56
|
+
const section = block?.sections.find(
|
|
57
|
+
section => section.headerText() === sectionName &&
|
|
58
|
+
(treeScopeDistance === -1 || section.treeScopeDistance() === treeScopeDistance));
|
|
57
59
|
if (!section || !block) {
|
|
58
60
|
return;
|
|
59
61
|
}
|