chrome-devtools-frontend 1.0.1582745 → 1.0.1585538
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/front_end/core/host/InspectorFrontendHostAPI.ts +1 -0
- package/front_end/core/host/InspectorFrontendHostStub.ts +2 -0
- package/front_end/core/root/Runtime.ts +12 -11
- package/front_end/core/sdk/CSSMatchedStyles.ts +3 -0
- package/front_end/core/sdk/CSSMetadata.ts +3 -0
- package/front_end/core/sdk/NetworkManager.ts +63 -115
- package/front_end/entrypoint_template.html +1 -5
- package/front_end/entrypoints/main/MainImpl.ts +1 -0
- package/front_end/generated/Deprecation.ts +21 -0
- package/front_end/generated/InspectorBackendCommands.ts +5 -3
- package/front_end/generated/SupportedCSSProperties.js +6 -2
- package/front_end/generated/protocol-mapping.d.ts +2 -1
- package/front_end/generated/protocol-proxy-api.d.ts +2 -1
- package/front_end/generated/protocol.ts +14 -0
- package/front_end/models/ai_assistance/AiConversation.ts +34 -2
- package/front_end/models/ai_assistance/BuiltInAi.ts +1 -2
- package/front_end/models/ai_assistance/agents/AiAgent.ts +3 -10
- package/front_end/models/ai_assistance/agents/ContextSelectionAgent.snapshot.txt +1 -1
- package/front_end/models/ai_assistance/agents/ContextSelectionAgent.ts +17 -9
- package/front_end/models/ai_assistance/agents/StylingAgent.snapshot.txt +0 -24
- package/front_end/models/ai_code_generation/AiCodeGeneration.ts +5 -0
- package/front_end/models/bindings/DebuggerWorkspaceBinding.ts +16 -8
- package/front_end/models/computed_style/ComputedStyleModel.ts +20 -15
- package/front_end/models/greendev/Prototypes.ts +1 -5
- package/front_end/models/issues_manager/CorsIssue.ts +2 -9
- package/front_end/models/issues_manager/descriptions/corsLocalNetworkAccessPermissionDenied.md +2 -2
- package/front_end/models/javascript_metadata/NativeFunctions.js +20 -0
- package/front_end/panels/ai_assistance/AiAssistancePanel.ts +28 -39
- package/front_end/panels/ai_assistance/components/ChatInput.ts +73 -95
- package/front_end/panels/ai_assistance/components/ChatMessage.ts +4 -2
- package/front_end/panels/ai_assistance/components/ChatView.ts +0 -2
- package/front_end/panels/autofill/AutofillView.ts +4 -8
- package/front_end/panels/common/AiCodeGenerationTeaser.ts +37 -8
- package/front_end/panels/console/ConsoleView.ts +1 -2
- package/front_end/panels/console/ConsoleViewMessage.ts +18 -11
- package/front_end/panels/elements/ComputedStyleWidget.ts +80 -48
- package/front_end/panels/elements/ElementStatePaneWidget.ts +1 -1
- package/front_end/panels/elements/ElementsPanel.ts +18 -5
- package/front_end/panels/elements/ElementsTreeElement.ts +459 -435
- package/front_end/panels/elements/EventListenersWidget.ts +2 -4
- package/front_end/panels/elements/NodeStackTraceWidget.ts +1 -3
- package/front_end/panels/elements/PropertiesWidget.ts +1 -2
- package/front_end/panels/elements/StylePropertyTreeElement.ts +18 -2
- package/front_end/panels/elements/computedStyleWidget.css +25 -0
- package/front_end/panels/issues/AffectedResourcesView.ts +1 -2
- package/front_end/panels/network/NetworkDataGridNode.ts +0 -8
- package/front_end/panels/network/NetworkLogView.ts +64 -105
- package/front_end/panels/network/RequestConditionsDrawer.ts +168 -233
- package/front_end/panels/network/network-meta.ts +4 -27
- package/front_end/panels/settings/SettingsScreen.ts +12 -4
- package/front_end/panels/settings/WorkspaceSettingsTab.ts +1 -1
- package/front_end/panels/snippets/SnippetsQuickOpen.ts +6 -9
- package/front_end/panels/sources/CallStackSidebarPane.ts +6 -8
- package/front_end/panels/sources/DebuggerPlugin.ts +2 -1
- package/front_end/panels/sources/GoToLineQuickOpen.ts +0 -4
- package/front_end/panels/sources/OpenFileQuickOpen.ts +0 -4
- package/front_end/panels/sources/OutlineQuickOpen.ts +0 -4
- package/front_end/panels/sources/WatchExpressionsSidebarPane.ts +3 -1
- package/front_end/panels/sources/sources-meta.ts +9 -6
- package/front_end/panels/timeline/TimelineFlameChartView.ts +0 -12
- package/front_end/panels/timeline/TimelinePanel.ts +35 -15
- package/front_end/panels/timeline/components/Sidebar.ts +21 -0
- package/front_end/panels/timeline/components/insights/BaseInsightComponent.ts +1 -20
- package/front_end/panels/timeline/components/insights/InsightRenderer.ts +0 -1
- package/front_end/third_party/chromium/README.chromium +1 -1
- package/front_end/third_party/puppeteer/README.chromium +2 -2
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/core/BrowsingContext.d.ts +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/core/BrowsingContext.d.ts.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/core/BrowsingContext.js +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/core/BrowsingContext.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/node/ChromeLauncher.d.ts.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/node/ChromeLauncher.js +2 -3
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/node/ChromeLauncher.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.d.ts +2 -2
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.js +2 -2
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/encoding.d.ts.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/encoding.js +5 -3
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/encoding.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/version.d.ts +1 -1
- package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/version.js +1 -1
- package/front_end/third_party/puppeteer/package/lib/es5-iife/puppeteer-core-browser.js +8 -6
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/core/BrowsingContext.d.ts +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/core/BrowsingContext.d.ts.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/core/BrowsingContext.js +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/core/BrowsingContext.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/node/ChromeLauncher.d.ts.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/node/ChromeLauncher.js +2 -3
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/node/ChromeLauncher.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.d.ts +2 -2
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.js +2 -2
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/util/encoding.d.ts.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/util/encoding.js +5 -3
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/util/encoding.js.map +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/util/version.d.ts +1 -1
- package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/util/version.js +1 -1
- package/front_end/third_party/puppeteer/package/package.json +4 -4
- package/front_end/third_party/puppeteer/package/src/bidi/Page.ts +1 -1
- package/front_end/third_party/puppeteer/package/src/bidi/core/BrowsingContext.ts +2 -2
- package/front_end/third_party/puppeteer/package/src/node/ChromeLauncher.ts +4 -3
- package/front_end/third_party/puppeteer/package/src/revisions.ts +2 -2
- package/front_end/third_party/puppeteer/package/src/util/encoding.ts +5 -3
- package/front_end/third_party/puppeteer/package/src/util/version.ts +1 -1
- package/front_end/ui/components/linkifier/LinkifierImpl.ts +3 -0
- package/front_end/ui/components/linkifier/LinkifierUtils.ts +3 -0
- package/front_end/ui/components/text_editor/AiCodeCompletionProvider.ts +8 -0
- package/front_end/ui/components/text_editor/AiCodeGenerationParser.ts +29 -7
- package/front_end/ui/components/text_editor/AiCodeGenerationProvider.ts +42 -18
- package/front_end/ui/components/text_editor/config.ts +6 -0
- package/front_end/ui/legacy/InspectorView.ts +41 -15
- package/front_end/ui/legacy/Toolbar.ts +12 -4
- package/front_end/ui/legacy/UIUtils.ts +56 -7
- package/front_end/ui/legacy/components/quick_open/CommandMenu.ts +4 -3
- package/front_end/ui/legacy/components/quick_open/FilteredListWidget.ts +2 -1
- package/front_end/ui/legacy/components/quick_open/HelpQuickOpen.ts +8 -2
- package/front_end/ui/legacy/components/quick_open/QuickOpen.ts +8 -8
- package/front_end/ui/legacy/components/utils/JSPresentationUtils.ts +12 -9
- package/front_end/ui/legacy/components/utils/Linkifier.ts +116 -87
- package/front_end/ui/legacy/legacy.ts +0 -2
- package/front_end/ui/visual_logging/KnownContextValues.ts +4 -0
- package/mcp/HostBindings.ts +3 -0
- package/package.json +1 -1
- package/front_end/models/issues_manager/descriptions/corsInsecurePrivateNetwork.md +0 -10
- package/front_end/models/issues_manager/descriptions/corsPreflightAllowPrivateNetworkError.md +0 -10
- package/front_end/models/issues_manager/descriptions/corsPrivateNetworkPermissionDenied.md +0 -10
- package/front_end/panels/elements/computedStyleSidebarPane.css +0 -18
- package/front_end/ui/legacy/Floaty.ts +0 -438
- package/front_end/ui/legacy/floaty.css +0 -77
|
@@ -178,8 +178,6 @@ export class ChromeLauncher extends BrowserLauncher {
|
|
|
178
178
|
'MediaRouter',
|
|
179
179
|
'OptimizationHints',
|
|
180
180
|
'RenderDocument', // https://crbug.com/444150315
|
|
181
|
-
'IPH_ReadingModePageActionLabel', // b/479237585
|
|
182
|
-
'ReadAnythingOmniboxChip', // b/479237585
|
|
183
181
|
...(turnOnExperimentalFeaturesForTesting
|
|
184
182
|
? []
|
|
185
183
|
: [
|
|
@@ -247,7 +245,10 @@ export class ChromeLauncher extends BrowserLauncher {
|
|
|
247
245
|
enableExtensions = false,
|
|
248
246
|
} = options;
|
|
249
247
|
if (userDataDir) {
|
|
250
|
-
|
|
248
|
+
// If absolute (for any platform) path is given, we should not resolve it.
|
|
249
|
+
chromeArguments.push(
|
|
250
|
+
`--user-data-dir=${path.posix.isAbsolute(userDataDir) || path.win32.isAbsolute(userDataDir) ? userDataDir : path.resolve(userDataDir)}`,
|
|
251
|
+
);
|
|
251
252
|
}
|
|
252
253
|
if (devtools) {
|
|
253
254
|
chromeArguments.push('--auto-open-devtools-for-tabs');
|
|
@@ -12,9 +12,11 @@ export function stringToTypedArray(
|
|
|
12
12
|
base64Encoded = false,
|
|
13
13
|
): Uint8Array {
|
|
14
14
|
if (base64Encoded) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
if ('fromBase64' in Uint8Array) {
|
|
16
|
+
// @ts-expect-error fromBase64 is newer than the types we use.
|
|
17
|
+
return Uint8Array.fromBase64(string);
|
|
18
|
+
}
|
|
19
|
+
// TODO: remove Buffer in v26 when it becomes LTS.
|
|
18
20
|
if (typeof Buffer === 'function') {
|
|
19
21
|
return Buffer.from(string, 'base64');
|
|
20
22
|
}
|
|
@@ -31,6 +31,9 @@ export class LinkifierClick extends Event {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* @deprecated do not use
|
|
36
|
+
*/
|
|
34
37
|
export class Linkifier extends HTMLElement {
|
|
35
38
|
readonly #shadow = this.attachShadow({mode: 'open'});
|
|
36
39
|
#url: Platform.DevToolsPath.UrlString = Platform.DevToolsPath.EmptyUrlString;
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
import type * as Platform from '../../../core/platform/platform.js';
|
|
6
6
|
import * as Bindings from '../../../models/bindings/bindings.js';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* @deprecated do not use
|
|
10
|
+
*/
|
|
8
11
|
export function linkText(url: Platform.DevToolsPath.UrlString, lineNumber?: number): string {
|
|
9
12
|
if (url) {
|
|
10
13
|
const displayName = Bindings.ResourceUtils.displayNameForURL(url);
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
acceptAiAutoCompleteSuggestion,
|
|
19
19
|
aiAutoCompleteSuggestion,
|
|
20
20
|
aiAutoCompleteSuggestionState,
|
|
21
|
+
AiSuggestionSource,
|
|
21
22
|
hasActiveAiSuggestion,
|
|
22
23
|
setAiAutoCompleteSuggestion,
|
|
23
24
|
showCompletionHint,
|
|
@@ -201,6 +202,12 @@ export class AiCodeCompletionProvider {
|
|
|
201
202
|
if (!this.#aiCodeCompletion || !this.#editor || !hasActiveAiSuggestion(this.#editor.state)) {
|
|
202
203
|
return false;
|
|
203
204
|
}
|
|
205
|
+
if (this.#editor.state.field(aiAutoCompleteSuggestionState)?.source === AiSuggestionSource.GENERATION) {
|
|
206
|
+
// If the suggestion is from code generation, we don't want to
|
|
207
|
+
// dismiss it here. The user should use the code generation
|
|
208
|
+
// provider's keymap to dismiss the suggestion.
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
204
211
|
this.#editor.dispatch({
|
|
205
212
|
effects: setAiAutoCompleteSuggestion.of(null),
|
|
206
213
|
});
|
|
@@ -347,6 +354,7 @@ export class AiCodeCompletionProvider {
|
|
|
347
354
|
startTime,
|
|
348
355
|
clearCachedRequest: this.clearCache.bind(this),
|
|
349
356
|
onImpression: this.#aiCodeCompletion?.registerUserImpression.bind(this.#aiCodeCompletion),
|
|
357
|
+
source: AiSuggestionSource.COMPLETION,
|
|
350
358
|
})
|
|
351
359
|
});
|
|
352
360
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import * as CodeMirror from '../../../third_party/codemirror.next/codemirror.next.js';
|
|
6
6
|
|
|
7
|
-
const LINE_COMMENT_PATTERN = /^(?:\/\/|#)\s
|
|
7
|
+
const LINE_COMMENT_PATTERN = /^(?:\/\/|#)\s*/gm;
|
|
8
8
|
const BLOCK_COMMENT_START_PATTERN = /^\/\*+\s*/;
|
|
9
9
|
const BLOCK_COMMENT_END_PATTERN = /\s*\*+\/$/;
|
|
10
10
|
const BLOCK_COMMENT_LINE_PREFIX_PATTERN = /^\s*\*\s?/;
|
|
@@ -51,7 +51,7 @@ function resolveCommentNode(state: CodeMirror.EditorState, cursorPosition: numbe
|
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
function
|
|
54
|
+
function extractBlockCommentText(rawText: string): string|undefined {
|
|
55
55
|
// Remove /* and */, whitespace, and common leading asterisks on new lines
|
|
56
56
|
if (!rawText.match(BLOCK_COMMENT_START_PATTERN)) {
|
|
57
57
|
return;
|
|
@@ -66,8 +66,29 @@ function extractBlockComment(rawText: string): string|undefined {
|
|
|
66
66
|
return cleaned;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function extractLineComment(
|
|
70
|
-
|
|
69
|
+
function extractLineComment(node: CodeMirror.SyntaxNode, state: CodeMirror.EditorState): CommentNodeInfo|undefined {
|
|
70
|
+
let firstNode = node;
|
|
71
|
+
let lastNode = node;
|
|
72
|
+
|
|
73
|
+
let prev = node.prevSibling;
|
|
74
|
+
while (prev?.type.name.includes('LineComment')) {
|
|
75
|
+
firstNode = prev;
|
|
76
|
+
prev = prev.prevSibling;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let next = node.nextSibling;
|
|
80
|
+
while (next?.type.name.includes('LineComment')) {
|
|
81
|
+
lastNode = next;
|
|
82
|
+
next = next.nextSibling;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Extract all lines between the first and last identified node
|
|
86
|
+
const fullRawText = state.doc.sliceString(firstNode.from, lastNode.to);
|
|
87
|
+
|
|
88
|
+
// Process each line to remove prefixes (// or #)
|
|
89
|
+
const concatenatedText = fullRawText.replaceAll(LINE_COMMENT_PATTERN, '').replace(/\n\s*\n/g, '\n').trim();
|
|
90
|
+
|
|
91
|
+
return concatenatedText ? {text: concatenatedText, to: lastNode.to} : undefined;
|
|
71
92
|
}
|
|
72
93
|
|
|
73
94
|
export class AiCodeGenerationParser {
|
|
@@ -80,9 +101,10 @@ export class AiCodeGenerationParser {
|
|
|
80
101
|
const rawText = state.doc.sliceString(node.from, node.to);
|
|
81
102
|
let text = '';
|
|
82
103
|
if (nodeType.includes('LineComment')) {
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
104
|
+
return extractLineComment(node, state);
|
|
105
|
+
}
|
|
106
|
+
if (nodeType.includes('BlockComment')) {
|
|
107
|
+
text = extractBlockCommentText(rawText) ?? '';
|
|
86
108
|
} else {
|
|
87
109
|
text = rawText;
|
|
88
110
|
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
acceptAiAutoCompleteSuggestion,
|
|
20
20
|
aiAutoCompleteSuggestion,
|
|
21
21
|
aiAutoCompleteSuggestionState,
|
|
22
|
+
AiSuggestionSource,
|
|
22
23
|
hasActiveAiSuggestion,
|
|
23
24
|
setAiAutoCompleteSuggestion,
|
|
24
25
|
} from './config.js';
|
|
@@ -159,6 +160,12 @@ export class AiCodeGenerationProvider {
|
|
|
159
160
|
return false;
|
|
160
161
|
}
|
|
161
162
|
if (hasActiveAiSuggestion(this.#editor.state)) {
|
|
163
|
+
if (this.#editor.state.field(aiAutoCompleteSuggestionState)?.source === AiSuggestionSource.COMPLETION) {
|
|
164
|
+
// If the suggestion is from code completion, we don't want to
|
|
165
|
+
// dismiss it here. The user should use the code completion
|
|
166
|
+
// provider's keymap to dismiss the suggestion.
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
162
169
|
this.#dismissTeaserAndSuggestion();
|
|
163
170
|
return true;
|
|
164
171
|
}
|
|
@@ -175,20 +182,11 @@ export class AiCodeGenerationProvider {
|
|
|
175
182
|
},
|
|
176
183
|
{
|
|
177
184
|
key: 'Tab',
|
|
178
|
-
run: ()
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (!accepted) {
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
if (suggestion?.rpcGlobalId) {
|
|
187
|
-
this.#aiCodeGeneration.registerUserAcceptance(suggestion.rpcGlobalId, suggestion.sampleId);
|
|
188
|
-
}
|
|
189
|
-
this.#aiCodeGenerationConfig?.onSuggestionAccepted(this.#aiCodeGenerationCitations);
|
|
190
|
-
return true;
|
|
191
|
-
},
|
|
185
|
+
run: this.#acceptAiSuggestion.bind(this),
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
key: 'Enter',
|
|
189
|
+
run: this.#acceptAiSuggestion.bind(this),
|
|
192
190
|
},
|
|
193
191
|
{
|
|
194
192
|
any: (_view: unknown, event: KeyboardEvent) => {
|
|
@@ -241,6 +239,21 @@ export class AiCodeGenerationProvider {
|
|
|
241
239
|
});
|
|
242
240
|
}
|
|
243
241
|
|
|
242
|
+
#acceptAiSuggestion(): boolean {
|
|
243
|
+
if (!this.#aiCodeGeneration || !this.#editor || !hasActiveAiSuggestion(this.#editor.state)) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
const {accepted, suggestion} = acceptAiAutoCompleteSuggestion(this.#editor.editor);
|
|
247
|
+
if (!accepted) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
if (suggestion?.rpcGlobalId) {
|
|
251
|
+
this.#aiCodeGeneration.registerUserAcceptance(suggestion.rpcGlobalId, suggestion.sampleId);
|
|
252
|
+
}
|
|
253
|
+
this.#aiCodeGenerationConfig?.onSuggestionAccepted(this.#aiCodeGenerationCitations);
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
244
257
|
#activateTeaser(update: CodeMirror.ViewUpdate): void {
|
|
245
258
|
const currentTeaserMode = update.state.field(aiCodeGenerationTeaserModeState);
|
|
246
259
|
if (currentTeaserMode === AiCodeGenerationTeaserMode.ACTIVE) {
|
|
@@ -335,12 +348,13 @@ export class AiCodeGenerationProvider {
|
|
|
335
348
|
this.#editor.dispatch({
|
|
336
349
|
effects: [
|
|
337
350
|
setAiAutoCompleteSuggestion.of({
|
|
338
|
-
text: '\n' + suggestionText,
|
|
351
|
+
text: '\n' + suggestionText + '\n',
|
|
339
352
|
from: commentNodeInfo.to,
|
|
340
353
|
rpcGlobalId: generationResponse.metadata.rpcGlobalId,
|
|
341
354
|
sampleId: topSample.sampleId,
|
|
342
355
|
startTime,
|
|
343
356
|
onImpression: this.#aiCodeGeneration?.registerUserImpression.bind(this.#aiCodeGeneration),
|
|
357
|
+
source: AiSuggestionSource.GENERATION,
|
|
344
358
|
}),
|
|
345
359
|
setAiCodeGenerationTeaserMode.of(AiCodeGenerationTeaserMode.ACTIVE)
|
|
346
360
|
]
|
|
@@ -441,9 +455,19 @@ function aiCodeGenerationTeaserExtension(teaser: PanelCommon.AiCodeGenerationTea
|
|
|
441
455
|
// Required for mouse hover to propagate to the info button in teaser.
|
|
442
456
|
return (event.target instanceof Node && teaser.contentElement.contains(event.target));
|
|
443
457
|
},
|
|
444
|
-
mousedown(event: MouseEvent): boolean {
|
|
445
|
-
|
|
446
|
-
|
|
458
|
+
mousedown(event: MouseEvent, view: CodeMirror.EditorView): boolean {
|
|
459
|
+
if (!(event.target instanceof Node) || !teaser.contentElement.contains(event.target)) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
// On mouse click, move the cursor position to the end of the line.
|
|
463
|
+
const cursorPosition = view.state.selection.main.head;
|
|
464
|
+
const line = view.state.doc.lineAt(cursorPosition);
|
|
465
|
+
if (cursorPosition !== line.to) {
|
|
466
|
+
view.dispatch({selection: {anchor: line.to, head: line.to}});
|
|
467
|
+
}
|
|
468
|
+
// Explicitly focus the editor.
|
|
469
|
+
view.focus();
|
|
470
|
+
return true;
|
|
447
471
|
},
|
|
448
472
|
keydown(event: KeyboardEvent): boolean {
|
|
449
473
|
if (!UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlEquivalentKey(event) ||
|
|
@@ -483,6 +483,11 @@ export function contentIncludingHint(view: CM.EditorView): string {
|
|
|
483
483
|
|
|
484
484
|
export const setAiAutoCompleteSuggestion = CM.StateEffect.define<ActiveSuggestion|null>();
|
|
485
485
|
|
|
486
|
+
export const enum AiSuggestionSource {
|
|
487
|
+
COMPLETION = 'completion',
|
|
488
|
+
GENERATION = 'generation',
|
|
489
|
+
}
|
|
490
|
+
|
|
486
491
|
export interface ActiveSuggestion {
|
|
487
492
|
text: string;
|
|
488
493
|
from: number;
|
|
@@ -491,6 +496,7 @@ export interface ActiveSuggestion {
|
|
|
491
496
|
startTime: number;
|
|
492
497
|
onImpression: (rpcGlobalId: Host.AidaClient.RpcGlobalId, latency: number, sampleId?: number) => void;
|
|
493
498
|
clearCachedRequest?: () => void;
|
|
499
|
+
source: AiSuggestionSource;
|
|
494
500
|
}
|
|
495
501
|
|
|
496
502
|
export const aiAutoCompleteSuggestionState = CM.StateField.define<ActiveSuggestion|null>({
|
|
@@ -8,7 +8,6 @@ import * as Host from '../../core/host/host.js';
|
|
|
8
8
|
import * as i18n from '../../core/i18n/i18n.js';
|
|
9
9
|
import * as Root from '../../core/root/root.js';
|
|
10
10
|
import * as SDK from '../../core/sdk/sdk.js';
|
|
11
|
-
import * as GreenDev from '../../models/greendev/greendev.js';
|
|
12
11
|
import * as Buttons from '../../ui/components/buttons/buttons.js';
|
|
13
12
|
import {createIcon, type Icon} from '../kit/kit.js';
|
|
14
13
|
import * as VisualLogging from '../visual_logging/visual_logging.js';
|
|
@@ -20,7 +19,6 @@ import type {Context} from './Context.js';
|
|
|
20
19
|
import type {ContextMenu} from './ContextMenu.js';
|
|
21
20
|
import {Dialog} from './Dialog.js';
|
|
22
21
|
import {DockController, DockState, Events as DockControllerEvents} from './DockController.js';
|
|
23
|
-
import {Floaty} from './Floaty.js';
|
|
24
22
|
import {GlassPane} from './GlassPane.js';
|
|
25
23
|
import {Infobar, Type as InfobarType} from './Infobar.js';
|
|
26
24
|
import {KeyboardShortcut} from './KeyboardShortcut.js';
|
|
@@ -54,6 +52,14 @@ const UIStrings = {
|
|
|
54
52
|
* @description Title of an action that reloads the DevTools
|
|
55
53
|
*/
|
|
56
54
|
reloadDevtools: 'Reload DevTools',
|
|
55
|
+
/**
|
|
56
|
+
* @description Title of an action that restarts Chrome
|
|
57
|
+
*/
|
|
58
|
+
restartChrome: 'Restart Chrome',
|
|
59
|
+
/**
|
|
60
|
+
* @description Confirmation dialog text for restarting Chrome
|
|
61
|
+
*/
|
|
62
|
+
areYouSureYouWantToRestartChrome: 'Are you sure you want to restart Chrome?',
|
|
57
63
|
/**
|
|
58
64
|
* @description Text for context menu action to move a tab to the main tab bar
|
|
59
65
|
*/
|
|
@@ -155,6 +161,7 @@ export class InspectorView extends VBox implements ViewLocationResolver {
|
|
|
155
161
|
private focusRestorer?: WidgetFocusRestorer|null;
|
|
156
162
|
private ownerSplitWidget?: SplitWidget;
|
|
157
163
|
private reloadRequiredInfobar?: Infobar;
|
|
164
|
+
#chromeRestartRequiredInfobar?: Infobar;
|
|
158
165
|
#debuggedTabReloadRequiredInfobar?: Infobar;
|
|
159
166
|
#selectOverrideFolderInfobar?: Infobar;
|
|
160
167
|
#resizeObserver: ResizeObserver;
|
|
@@ -362,10 +369,6 @@ export class InspectorView extends VBox implements ViewLocationResolver {
|
|
|
362
369
|
this.element.style.setProperty('--devtools-window-top', `${rect.top}px`);
|
|
363
370
|
this.element.style.setProperty('--devtools-window-bottom', `${window.innerHeight - rect.bottom}px`);
|
|
364
371
|
this.element.style.setProperty('--devtools-window-height', `${rect.height}px`);
|
|
365
|
-
|
|
366
|
-
if (Floaty.exists()) {
|
|
367
|
-
Floaty.instance().setDevToolsRect(rect);
|
|
368
|
-
}
|
|
369
372
|
}
|
|
370
373
|
|
|
371
374
|
override wasShown(): void {
|
|
@@ -376,13 +379,6 @@ export class InspectorView extends VBox implements ViewLocationResolver {
|
|
|
376
379
|
DockController.instance().addEventListener(
|
|
377
380
|
DockControllerEvents.DOCK_SIDE_CHANGED, this.#applyDrawerOrientationForDockSide, this);
|
|
378
381
|
this.#applyDrawerOrientationForDockSide();
|
|
379
|
-
|
|
380
|
-
if (GreenDev.Prototypes.instance().isEnabled('inDevToolsFloaty')) {
|
|
381
|
-
Floaty.instance({
|
|
382
|
-
forceNew: true,
|
|
383
|
-
document: this.element.ownerDocument,
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
382
|
}
|
|
387
383
|
|
|
388
384
|
override willHide(): void {
|
|
@@ -676,7 +672,7 @@ export class InspectorView extends VBox implements ViewLocationResolver {
|
|
|
676
672
|
}
|
|
677
673
|
|
|
678
674
|
displayReloadRequiredWarning(message: string): void {
|
|
679
|
-
if (!this.reloadRequiredInfobar) {
|
|
675
|
+
if (!this.reloadRequiredInfobar && !this.#chromeRestartRequiredInfobar) {
|
|
680
676
|
const infobar = new Infobar(
|
|
681
677
|
InfobarType.INFO, message,
|
|
682
678
|
[
|
|
@@ -693,7 +689,37 @@ export class InspectorView extends VBox implements ViewLocationResolver {
|
|
|
693
689
|
this.attachInfobar(infobar);
|
|
694
690
|
this.reloadRequiredInfobar = infobar;
|
|
695
691
|
infobar.setCloseCallback(() => {
|
|
696
|
-
|
|
692
|
+
this.reloadRequiredInfobar = undefined;
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
displayChromeRestartRequiredWarning(message: string): void {
|
|
698
|
+
if (this.reloadRequiredInfobar) {
|
|
699
|
+
this.reloadRequiredInfobar.dispose();
|
|
700
|
+
}
|
|
701
|
+
if (!this.#chromeRestartRequiredInfobar) {
|
|
702
|
+
const infobar = new Infobar(
|
|
703
|
+
InfobarType.INFO, message,
|
|
704
|
+
[
|
|
705
|
+
{
|
|
706
|
+
text: i18nString(UIStrings.restartChrome),
|
|
707
|
+
delegate: () => {
|
|
708
|
+
if (confirm(i18nString(UIStrings.areYouSureYouWantToRestartChrome))) {
|
|
709
|
+
Host.InspectorFrontendHost.InspectorFrontendHostInstance.requestRestart();
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
dismiss: false,
|
|
713
|
+
buttonVariant: Buttons.Button.Variant.PRIMARY,
|
|
714
|
+
jslogContext: 'main.chrome-restart-chrome',
|
|
715
|
+
},
|
|
716
|
+
],
|
|
717
|
+
undefined, 'reload-required');
|
|
718
|
+
infobar.setParentView(this);
|
|
719
|
+
this.attachInfobar(infobar);
|
|
720
|
+
this.#chromeRestartRequiredInfobar = infobar;
|
|
721
|
+
infobar.setCloseCallback(() => {
|
|
722
|
+
this.#chromeRestartRequiredInfobar = undefined;
|
|
697
723
|
});
|
|
698
724
|
}
|
|
699
725
|
}
|
|
@@ -835,7 +835,7 @@ export class ToolbarInputElement extends HTMLElement {
|
|
|
835
835
|
|
|
836
836
|
item?: ToolbarInput;
|
|
837
837
|
datalist: HTMLDataListElement|null = null;
|
|
838
|
-
value: string|undefined = undefined;
|
|
838
|
+
#value: string|undefined = undefined;
|
|
839
839
|
#disabled = false;
|
|
840
840
|
|
|
841
841
|
connectedCallback(): void {
|
|
@@ -862,8 +862,8 @@ export class ToolbarInputElement extends HTMLElement {
|
|
|
862
862
|
/* shrinkFactor=*/ undefined, tooltip, this.datalist ? this.#onAutocomplete.bind(this) : undefined,
|
|
863
863
|
/* dynamicCompletions=*/ undefined, jslogContext, this);
|
|
864
864
|
}
|
|
865
|
-
if (this
|
|
866
|
-
this.item.setValue(this
|
|
865
|
+
if (this.#value) {
|
|
866
|
+
this.item.setValue(this.#value);
|
|
867
867
|
}
|
|
868
868
|
if (this.#disabled) {
|
|
869
869
|
this.item.setEnabled(false);
|
|
@@ -894,7 +894,7 @@ export class ToolbarInputElement extends HTMLElement {
|
|
|
894
894
|
if (this.item && this.item.value() !== newValue) {
|
|
895
895
|
this.item.setValue(newValue, true);
|
|
896
896
|
} else {
|
|
897
|
-
this
|
|
897
|
+
this.#value = newValue;
|
|
898
898
|
}
|
|
899
899
|
} else if (name === 'disabled') {
|
|
900
900
|
this.#disabled = typeof newValue === 'string';
|
|
@@ -904,6 +904,14 @@ export class ToolbarInputElement extends HTMLElement {
|
|
|
904
904
|
}
|
|
905
905
|
}
|
|
906
906
|
|
|
907
|
+
get value(): string {
|
|
908
|
+
return this.item ? this.item.value() : (this.#value ?? '');
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
set value(value: string) {
|
|
912
|
+
this.setAttribute('value', value);
|
|
913
|
+
}
|
|
914
|
+
|
|
907
915
|
set disabled(disabled: boolean) {
|
|
908
916
|
if (disabled) {
|
|
909
917
|
this.setAttribute('disabled', '');
|
|
@@ -697,19 +697,42 @@ export class ElementFocusRestorer {
|
|
|
697
697
|
export function runCSSAnimationOnce(element: Element, className: string): void {
|
|
698
698
|
function animationEndCallback(): void {
|
|
699
699
|
element.classList.remove(className);
|
|
700
|
-
element.removeEventListener('
|
|
700
|
+
element.removeEventListener('animationend', animationEndCallback, false);
|
|
701
701
|
element.removeEventListener('animationcancel', animationEndCallback, false);
|
|
702
702
|
}
|
|
703
703
|
|
|
704
|
-
if
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
element.addEventListener('webkitAnimationEnd', animationEndCallback, false);
|
|
704
|
+
// Remove class if it exists.
|
|
705
|
+
element.classList.toggle(className, /* force=*/ false);
|
|
706
|
+
element.addEventListener('animationend', animationEndCallback, false);
|
|
709
707
|
element.addEventListener('animationcancel', animationEndCallback, false);
|
|
710
708
|
element.classList.add(className);
|
|
711
709
|
}
|
|
712
710
|
|
|
711
|
+
class AnimateOnDirective extends Lit.Directive.Directive {
|
|
712
|
+
#previousValue = false;
|
|
713
|
+
|
|
714
|
+
render(_condition: boolean, _className: string): void {
|
|
715
|
+
return undefined; // Directives don't have to render HTML
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
override update(part: Lit.Directive.ElementPart, [condition, className]: [boolean, string]): void {
|
|
719
|
+
const el = part.element as HTMLElement;
|
|
720
|
+
|
|
721
|
+
// Only trigger if the condition transitioned from false -> true
|
|
722
|
+
if (condition && !this.#previousValue) {
|
|
723
|
+
this.#animate(el, className);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
this.#previousValue = condition;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
#animate(el: HTMLElement, className: string): void {
|
|
730
|
+
runCSSAnimationOnce(el, className);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export const animateOn = Lit.Directive.directive(AnimateOnDirective);
|
|
735
|
+
|
|
713
736
|
export function measurePreferredSize(element: Element, containerElement?: Element|null): Geometry.Size {
|
|
714
737
|
const oldParent = element.parentElement;
|
|
715
738
|
const oldNextSibling = element.nextSibling;
|
|
@@ -1530,6 +1553,7 @@ export function createFileSelectorElement(callback: (arg0: File) => void, accept
|
|
|
1530
1553
|
}
|
|
1531
1554
|
|
|
1532
1555
|
export const MaxLengthForDisplayedURLs = 150;
|
|
1556
|
+
export const MaxLengthForDisplayedURLsInConsole = 40;
|
|
1533
1557
|
|
|
1534
1558
|
export class MessageDialog {
|
|
1535
1559
|
static async show(header: string, message: string, where?: Element|Document, jslogContext?: string): Promise<void> {
|
|
@@ -2193,13 +2217,29 @@ export const bindCheckboxImpl = function(
|
|
|
2193
2217
|
};
|
|
2194
2218
|
};
|
|
2195
2219
|
|
|
2220
|
+
export type BindToSettingOpts = ((newSettingValue: string) => boolean)|{
|
|
2221
|
+
validator?: (newSettingValue: string) => boolean,
|
|
2222
|
+
jslog?: boolean,
|
|
2223
|
+
};
|
|
2196
2224
|
export const bindToSetting =
|
|
2197
2225
|
(settingOrName: string|Common.Settings.Setting<boolean|string>|Common.Settings.RegExpSetting,
|
|
2198
|
-
|
|
2226
|
+
optionsOrValidator?: BindToSettingOpts): ReturnType<typeof Directives.ref> => {
|
|
2199
2227
|
const setting = typeof settingOrName === 'string' ?
|
|
2200
2228
|
Common.Settings.Settings.instance().moduleSetting(settingOrName) :
|
|
2201
2229
|
settingOrName;
|
|
2202
2230
|
|
|
2231
|
+
let stringValidator: ((newSettingValue: string) => boolean)|undefined;
|
|
2232
|
+
let jslog = true;
|
|
2233
|
+
if (typeof optionsOrValidator === 'function') {
|
|
2234
|
+
stringValidator = optionsOrValidator;
|
|
2235
|
+
} else if (optionsOrValidator) {
|
|
2236
|
+
stringValidator = optionsOrValidator.validator;
|
|
2237
|
+
if (optionsOrValidator.jslog !== undefined) {
|
|
2238
|
+
jslog = optionsOrValidator.jslog;
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
const jslogBuilder = jslog ? VisualLogging.toggle(setting.name).track({change: true}) : null;
|
|
2203
2243
|
// We can't use `setValue` as the change listener directly, otherwise we won't
|
|
2204
2244
|
// be able to remove it again.
|
|
2205
2245
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -2214,6 +2254,9 @@ export const bindToSetting =
|
|
|
2214
2254
|
setting.removeChangeListener(settingChanged);
|
|
2215
2255
|
return;
|
|
2216
2256
|
}
|
|
2257
|
+
if (jslogBuilder) {
|
|
2258
|
+
e.setAttribute('jslog', jslogBuilder.toString());
|
|
2259
|
+
}
|
|
2217
2260
|
|
|
2218
2261
|
setting.addChangeListener(settingChanged);
|
|
2219
2262
|
setValue =
|
|
@@ -2229,6 +2272,9 @@ export const bindToSetting =
|
|
|
2229
2272
|
return;
|
|
2230
2273
|
}
|
|
2231
2274
|
|
|
2275
|
+
if (jslogBuilder) {
|
|
2276
|
+
e.setAttribute('jslog', jslogBuilder.toString());
|
|
2277
|
+
}
|
|
2232
2278
|
setting.addChangeListener(settingChanged);
|
|
2233
2279
|
setValue = bindInput(e as HTMLInputElement, setting.set.bind(setting), (value: string) => {
|
|
2234
2280
|
try {
|
|
@@ -2249,6 +2295,9 @@ export const bindToSetting =
|
|
|
2249
2295
|
return;
|
|
2250
2296
|
}
|
|
2251
2297
|
|
|
2298
|
+
if (jslogBuilder) {
|
|
2299
|
+
e.setAttribute('jslog', jslogBuilder.toString());
|
|
2300
|
+
}
|
|
2252
2301
|
setting.addChangeListener(settingChanged);
|
|
2253
2302
|
setValue = bindInput(
|
|
2254
2303
|
e as HTMLInputElement, setting.set.bind(setting), stringValidator ?? (() => true), /* numeric */ false);
|
|
@@ -275,8 +275,8 @@ export const enum PanelOrDrawer {
|
|
|
275
275
|
export class CommandMenuProvider extends Provider {
|
|
276
276
|
private commands: Command[];
|
|
277
277
|
|
|
278
|
-
constructor(commandsForTest: Command[] = []) {
|
|
279
|
-
super(
|
|
278
|
+
constructor(jslogContext: string, commandsForTest: Command[] = []) {
|
|
279
|
+
super(jslogContext);
|
|
280
280
|
this.commands = commandsForTest;
|
|
281
281
|
}
|
|
282
282
|
|
|
@@ -455,8 +455,9 @@ export class ShowActionDelegate implements UI.ActionRegistration.ActionDelegate
|
|
|
455
455
|
registerProvider({
|
|
456
456
|
prefix: '>',
|
|
457
457
|
iconName: 'chevron-right',
|
|
458
|
-
provider: () => Promise.resolve(new CommandMenuProvider()),
|
|
458
|
+
provider: (jslogContext: string) => Promise.resolve(new CommandMenuProvider(jslogContext)),
|
|
459
459
|
helpTitle: () => i18nString(UIStrings.runCommand),
|
|
460
460
|
titlePrefix: () => i18nString(UIStrings.run),
|
|
461
461
|
titleSuggestion: () => i18nString(UIStrings.command),
|
|
462
|
+
jslogContext: 'command',
|
|
462
463
|
});
|
|
@@ -657,8 +657,9 @@ export function getRegisteredProviders(): ProviderRegistration[] {
|
|
|
657
657
|
export interface ProviderRegistration {
|
|
658
658
|
prefix: string;
|
|
659
659
|
iconName: string;
|
|
660
|
-
provider: () => Promise<Provider>;
|
|
660
|
+
provider: (jslogContext: string) => Promise<Provider>;
|
|
661
661
|
helpTitle: (() => string);
|
|
662
662
|
titlePrefix: (() => string);
|
|
663
663
|
titleSuggestion?: (() => string);
|
|
664
|
+
jslogContext: string;
|
|
664
665
|
}
|
|
@@ -24,11 +24,16 @@ export class HelpQuickOpen extends Provider {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
private async addProvider(extension: ProviderRegistration): Promise<void> {
|
|
27
|
+
// We want to exclude Help menu as we are already in it.
|
|
28
|
+
if (extension.prefix === '?') {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
this.providers.push({
|
|
28
33
|
prefix: extension.prefix || '',
|
|
29
34
|
iconName: extension.iconName,
|
|
30
35
|
title: extension.helpTitle(),
|
|
31
|
-
jslogContext:
|
|
36
|
+
jslogContext: extension.jslogContext,
|
|
32
37
|
});
|
|
33
38
|
}
|
|
34
39
|
|
|
@@ -69,8 +74,9 @@ export class HelpQuickOpen extends Provider {
|
|
|
69
74
|
registerProvider({
|
|
70
75
|
prefix: '?',
|
|
71
76
|
iconName: 'help',
|
|
72
|
-
provider:
|
|
77
|
+
provider: jslogContext => Promise.resolve(new HelpQuickOpen(jslogContext)),
|
|
73
78
|
helpTitle: () => 'Help',
|
|
74
79
|
titlePrefix: () => 'Help',
|
|
75
80
|
titleSuggestion: undefined,
|
|
81
|
+
jslogContext: 'help'
|
|
76
82
|
});
|
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
import * as i18n from '../../../../core/i18n/i18n.js';
|
|
6
6
|
import type * as UI from '../../legacy.js';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
FilteredListWidget,
|
|
10
|
+
getRegisteredProviders,
|
|
11
|
+
type Provider,
|
|
12
|
+
type ProviderRegistration
|
|
13
|
+
} from './FilteredListWidget.js';
|
|
9
14
|
|
|
10
15
|
const UIStrings = {
|
|
11
16
|
/**
|
|
@@ -42,19 +47,14 @@ export class QuickOpenImpl {
|
|
|
42
47
|
filteredListWidget.setQuery(query);
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
private addProvider(extension: {
|
|
46
|
-
prefix: string,
|
|
47
|
-
provider: () => Promise<Provider>,
|
|
48
|
-
titlePrefix: () => string,
|
|
49
|
-
titleSuggestion?: (() => string),
|
|
50
|
-
}): void {
|
|
50
|
+
private addProvider(extension: ProviderRegistration): void {
|
|
51
51
|
const prefix = extension.prefix;
|
|
52
52
|
if (prefix === null) {
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
this.prefixes.push(prefix);
|
|
56
56
|
this.providers.set(prefix, {
|
|
57
|
-
provider: extension.provider,
|
|
57
|
+
provider: () => extension.provider(extension.jslogContext),
|
|
58
58
|
titlePrefix: extension.titlePrefix,
|
|
59
59
|
titleSuggestion: extension.titleSuggestion,
|
|
60
60
|
});
|