chrome-devtools-frontend 1.0.1563104 → 1.0.1563563
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/AUTHORS +1 -0
- package/front_end/core/sdk/CSSMatchedStyles.ts +46 -10
- package/front_end/models/ai_code_completion/AiCodeCompletion.ts +2 -190
- package/front_end/panels/ai_assistance/components/ChatInput.ts +22 -0
- package/front_end/panels/common/AiCodeCompletionDisclaimer.ts +39 -5
- package/front_end/panels/common/AiCodeCompletionSummaryToolbar.ts +7 -0
- package/front_end/panels/common/AiCodeGenerationTeaser.ts +70 -3
- package/front_end/panels/common/aiCodeGenerationTeaser.css +30 -0
- package/front_end/panels/console/ConsoleView.ts +2 -1
- package/front_end/panels/sources/AiCodeCompletionPlugin.ts +6 -2
- package/front_end/ui/components/text_editor/AiCodeCompletionProvider.ts +26 -8
- package/front_end/ui/components/text_editor/AiCodeGenerationParser.ts +77 -0
- package/front_end/ui/components/text_editor/AiCodeGenerationProvider.ts +57 -39
- package/front_end/ui/components/text_editor/text_editor.ts +1 -0
- package/package.json +1 -1
package/AUTHORS
CHANGED
|
@@ -36,6 +36,7 @@ Douglas Chiang <douglasdothnc@gmail.com>
|
|
|
36
36
|
Dragonish <no.web.developer@outlook.com>
|
|
37
37
|
Eden Wang <nedenwang@gmail.com>
|
|
38
38
|
Edward Trist <edwardtrist@gmail.com>
|
|
39
|
+
Emir D <emrd434@gmail.com>
|
|
39
40
|
Ergün Erdoğmuş <erdogmusergun@gmail.com>
|
|
40
41
|
Eric Rannaud <eric.rannaud@gmail.com>
|
|
41
42
|
Faisal Salman <fyzlman@gmail.com>
|
|
@@ -649,12 +649,13 @@ export class CSSMatchedStyles {
|
|
|
649
649
|
// DOMInheritanceCascades.
|
|
650
650
|
for (const [pseudoType, nodeCascade] of pseudoCascades.entries()) {
|
|
651
651
|
pseudoInheritanceCascades.set(
|
|
652
|
-
pseudoType, new DOMInheritanceCascade(this, nodeCascade, this.#registeredProperties));
|
|
652
|
+
pseudoType, new DOMInheritanceCascade(this, nodeCascade, this.#registeredProperties, this.#mainDOMCascade));
|
|
653
653
|
}
|
|
654
654
|
|
|
655
655
|
for (const [highlightName, nodeCascade] of customHighlightPseudoCascades.entries()) {
|
|
656
656
|
customHighlightPseudoInheritanceCascades.set(
|
|
657
|
-
highlightName,
|
|
657
|
+
highlightName,
|
|
658
|
+
new DOMInheritanceCascade(this, nodeCascade, this.#registeredProperties, this.#mainDOMCascade));
|
|
658
659
|
}
|
|
659
660
|
|
|
660
661
|
return [pseudoInheritanceCascades, customHighlightPseudoInheritanceCascades];
|
|
@@ -959,17 +960,15 @@ class NodeCascade {
|
|
|
959
960
|
#matchedStyles: CSSMatchedStyles;
|
|
960
961
|
readonly styles: CSSStyleDeclaration[];
|
|
961
962
|
readonly #isInherited: boolean;
|
|
962
|
-
readonly #isHighlightPseudoCascade: boolean;
|
|
963
963
|
readonly propertiesState = new Map<CSSProperty, PropertyState>();
|
|
964
964
|
readonly activeProperties = new Map<string, CSSProperty>();
|
|
965
965
|
readonly #node: DOMNode;
|
|
966
966
|
constructor(
|
|
967
967
|
matchedStyles: CSSMatchedStyles, styles: CSSStyleDeclaration[], node: DOMNode, isInherited: boolean,
|
|
968
|
-
isHighlightPseudoCascade = false) {
|
|
968
|
+
readonly isHighlightPseudoCascade = false) {
|
|
969
969
|
this.#matchedStyles = matchedStyles;
|
|
970
970
|
this.styles = styles;
|
|
971
971
|
this.#isInherited = isInherited;
|
|
972
|
-
this.#isHighlightPseudoCascade = isHighlightPseudoCascade;
|
|
973
972
|
this.#node = node;
|
|
974
973
|
}
|
|
975
974
|
|
|
@@ -992,9 +991,16 @@ class NodeCascade {
|
|
|
992
991
|
// Do not pick non-inherited properties from inherited styles.
|
|
993
992
|
const metadata = cssMetadata();
|
|
994
993
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
994
|
+
if (this.#isInherited) {
|
|
995
|
+
if (this.isHighlightPseudoCascade) {
|
|
996
|
+
// All properties are inherited for highlight pseudos, but custom
|
|
997
|
+
// variables do not come from the inherited pseudo elements.
|
|
998
|
+
if (property.name.startsWith('--')) {
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
} else if (!metadata.isPropertyInherited(property.name)) {
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
998
1004
|
}
|
|
999
1005
|
|
|
1000
1006
|
// When a property does not have a range in an otherwise ranged CSSStyleDeclaration,
|
|
@@ -1085,6 +1091,7 @@ class NodeCascade {
|
|
|
1085
1091
|
function isRegular(declaration: CSSProperty|CSSRegisteredProperty): declaration is CSSProperty {
|
|
1086
1092
|
return 'ownerStyle' in declaration;
|
|
1087
1093
|
}
|
|
1094
|
+
|
|
1088
1095
|
export class CSSValueSource {
|
|
1089
1096
|
readonly declaration: CSSProperty|CSSRegisteredProperty;
|
|
1090
1097
|
constructor(declaration: CSSProperty|CSSRegisteredProperty) {
|
|
@@ -1176,15 +1183,27 @@ class DOMInheritanceCascade {
|
|
|
1176
1183
|
readonly #nodeCascades: NodeCascade[];
|
|
1177
1184
|
#registeredProperties: CSSRegisteredProperty[];
|
|
1178
1185
|
readonly #matchedStyles: CSSMatchedStyles;
|
|
1186
|
+
readonly #fallbackCascade: DOMInheritanceCascade|null = null;
|
|
1187
|
+
readonly #styles: CSSStyleDeclaration[] = [];
|
|
1179
1188
|
constructor(
|
|
1180
|
-
matchedStyles: CSSMatchedStyles, nodeCascades: NodeCascade[], registeredProperties: CSSRegisteredProperty[]
|
|
1189
|
+
matchedStyles: CSSMatchedStyles, nodeCascades: NodeCascade[], registeredProperties: CSSRegisteredProperty[],
|
|
1190
|
+
fallbackCascade: DOMInheritanceCascade|null = null) {
|
|
1181
1191
|
this.#nodeCascades = nodeCascades;
|
|
1182
1192
|
this.#matchedStyles = matchedStyles;
|
|
1183
1193
|
this.#registeredProperties = registeredProperties;
|
|
1194
|
+
this.#fallbackCascade = fallbackCascade;
|
|
1184
1195
|
|
|
1185
1196
|
for (const nodeCascade of nodeCascades) {
|
|
1186
1197
|
for (const style of nodeCascade.styles) {
|
|
1187
1198
|
this.#styleToNodeCascade.set(style, nodeCascade);
|
|
1199
|
+
this.#styles.push(style);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
if (fallbackCascade) {
|
|
1203
|
+
for (const [style, nodeCascade] of fallbackCascade.#styleToNodeCascade) {
|
|
1204
|
+
if (!this.#styles.includes(style)) {
|
|
1205
|
+
this.#styleToNodeCascade.set(style, nodeCascade);
|
|
1206
|
+
}
|
|
1188
1207
|
}
|
|
1189
1208
|
}
|
|
1190
1209
|
}
|
|
@@ -1248,6 +1267,9 @@ class DOMInheritanceCascade {
|
|
|
1248
1267
|
}
|
|
1249
1268
|
}
|
|
1250
1269
|
}
|
|
1270
|
+
if (this.#fallbackCascade && (!nodeCascade.isHighlightPseudoCascade || property.name.startsWith('--'))) {
|
|
1271
|
+
return this.#fallbackCascade.resolveProperty(property.name, property.ownerStyle);
|
|
1272
|
+
}
|
|
1251
1273
|
return null;
|
|
1252
1274
|
}
|
|
1253
1275
|
|
|
@@ -1538,7 +1560,7 @@ class DOMInheritanceCascade {
|
|
|
1538
1560
|
}
|
|
1539
1561
|
|
|
1540
1562
|
styles(): CSSStyleDeclaration[] {
|
|
1541
|
-
return
|
|
1563
|
+
return this.#styles;
|
|
1542
1564
|
}
|
|
1543
1565
|
|
|
1544
1566
|
propertyState(property: CSSProperty): PropertyState|null {
|
|
@@ -1610,6 +1632,20 @@ class DOMInheritanceCascade {
|
|
|
1610
1632
|
rule.propertyName(),
|
|
1611
1633
|
initialValue !== null ? {value: initialValue, declaration: new CSSValueSource(rule)} : null);
|
|
1612
1634
|
}
|
|
1635
|
+
if (this.#fallbackCascade) {
|
|
1636
|
+
this.#fallbackCascade.ensureInitialized();
|
|
1637
|
+
for (const [cascade, available] of this.#fallbackCascade.#availableCSSVariables) {
|
|
1638
|
+
this.#availableCSSVariables.set(cascade, available);
|
|
1639
|
+
}
|
|
1640
|
+
for (const [cascade, computed] of this.#fallbackCascade.#computedCSSVariables) {
|
|
1641
|
+
this.#computedCSSVariables.set(cascade, computed);
|
|
1642
|
+
}
|
|
1643
|
+
for (const [key, value] of this.#fallbackCascade.#availableCSSVariables.get(
|
|
1644
|
+
this.#fallbackCascade.#nodeCascades[0]) ??
|
|
1645
|
+
[]) {
|
|
1646
|
+
accumulatedCSSVariables.set(key, value);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1613
1649
|
for (let i = this.#nodeCascades.length - 1; i >= 0; --i) {
|
|
1614
1650
|
const nodeCascade = this.#nodeCascades[i];
|
|
1615
1651
|
const variableNames = [];
|
|
@@ -2,15 +2,11 @@
|
|
|
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 Common from '../../core/common/common.js';
|
|
6
5
|
import * as Host from '../../core/host/host.js';
|
|
7
6
|
import * as Root from '../../core/root/root.js';
|
|
8
7
|
|
|
9
8
|
import {debugLog} from './debug.js';
|
|
10
9
|
|
|
11
|
-
export const DELAY_BEFORE_SHOWING_RESPONSE_MS = 500;
|
|
12
|
-
export const AIDA_REQUEST_DEBOUNCE_TIMEOUT_MS = 200;
|
|
13
|
-
|
|
14
10
|
/**
|
|
15
11
|
* TODO(b/404796739): Remove these definitions of AgentOptions and RequestOptions and
|
|
16
12
|
* use the existing ones which are used for AI assistance panel agents.
|
|
@@ -140,20 +136,9 @@ const console = {
|
|
|
140
136
|
|
|
141
137
|
/**
|
|
142
138
|
* The AiCodeCompletion class is responsible for fetching code completion suggestions
|
|
143
|
-
* from the AIDA backend
|
|
144
|
-
*
|
|
145
|
-
* 1. **Debouncing requests:** As the user types, we don't want to send a request
|
|
146
|
-
* for every keystroke. Instead, we use debouncing to schedule a request
|
|
147
|
-
* only after the user has paused typing for a short period
|
|
148
|
-
* (AIDA_REQUEST_THROTTLER_TIMEOUT_MS). This prevents spamming the backend with
|
|
149
|
-
* requests for intermediate typing states.
|
|
150
|
-
*
|
|
151
|
-
* 2. **Delaying suggestions:** When a suggestion is received from the AIDA
|
|
152
|
-
* backend, we don't show it immediately. There is a minimum delay
|
|
153
|
-
* (DELAY_BEFORE_SHOWING_RESPONSE_MS) from when the request was sent to when
|
|
154
|
-
* the suggestion is displayed.
|
|
139
|
+
* from the AIDA backend.
|
|
155
140
|
*/
|
|
156
|
-
export class AiCodeCompletion
|
|
141
|
+
export class AiCodeCompletion {
|
|
157
142
|
#stopSequences: string[];
|
|
158
143
|
#renderingTimeout?: number;
|
|
159
144
|
#aidaRequestCache?: CachedRequest;
|
|
@@ -166,7 +151,6 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
|
|
|
166
151
|
readonly #serverSideLoggingEnabled: boolean;
|
|
167
152
|
|
|
168
153
|
constructor(opts: AgentOptions, panel: ContextFlavor, callbacks?: Callbacks, stopSequences?: string[]) {
|
|
169
|
-
super();
|
|
170
154
|
this.#aidaClient = opts.aidaClient;
|
|
171
155
|
this.#serverSideLoggingEnabled = opts.serverSideLoggingEnabled ?? false;
|
|
172
156
|
this.#panel = panel;
|
|
@@ -174,14 +158,6 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
|
|
|
174
158
|
this.#callbacks = callbacks;
|
|
175
159
|
}
|
|
176
160
|
|
|
177
|
-
#debouncedRequestAidaSuggestion = Common.Debouncer.debounce(
|
|
178
|
-
(prefix: string, suffix: string, cursorPositionAtRequest: number,
|
|
179
|
-
inferenceLanguage?: Host.AidaClient.AidaInferenceLanguage) => {
|
|
180
|
-
void this.#requestAidaSuggestion(
|
|
181
|
-
this.#buildRequest(prefix, suffix, inferenceLanguage), cursorPositionAtRequest);
|
|
182
|
-
},
|
|
183
|
-
AIDA_REQUEST_DEBOUNCE_TIMEOUT_MS);
|
|
184
|
-
|
|
185
161
|
#buildRequest(
|
|
186
162
|
prefix: string, suffix: string,
|
|
187
163
|
inferenceLanguage: Host.AidaClient.AidaInferenceLanguage = Host.AidaClient.AidaInferenceLanguage.JAVASCRIPT,
|
|
@@ -246,125 +222,6 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
|
|
|
246
222
|
};
|
|
247
223
|
}
|
|
248
224
|
|
|
249
|
-
#pickSampleFromResponse(response: Host.AidaClient.CompletionResponse): Host.AidaClient.GenerationSample|null {
|
|
250
|
-
if (!response.generatedSamples.length) {
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// `currentHint` is the portion of a standard autocomplete suggestion that the user has not yet typed.
|
|
255
|
-
// For example, if the user types `document.queryS` and the autocomplete suggests `document.querySelector`,
|
|
256
|
-
// the `currentHint` is `elector`.
|
|
257
|
-
const currentHintInMenu = this.#callbacks?.getCompletionHint();
|
|
258
|
-
// TODO(ergunsh): We should not do this check here. Instead, the AI code suggestions should be provided
|
|
259
|
-
// as it is to the view plugin. The view plugin should choose which one to use based on the completion hint
|
|
260
|
-
// and selected completion.
|
|
261
|
-
if (!currentHintInMenu) {
|
|
262
|
-
return response.generatedSamples[0];
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// TODO(ergunsh): This does not handle looking for `selectedCompletion`. The `currentHint` is `null`
|
|
266
|
-
// for the Sources panel case.
|
|
267
|
-
// Even though there is no match, we still return the first suggestion which will be displayed
|
|
268
|
-
// when the traditional autocomplete menu is closed.
|
|
269
|
-
return response.generatedSamples.find(sample => sample.generationString.startsWith(currentHintInMenu)) ??
|
|
270
|
-
response.generatedSamples[0];
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
async #generateSampleForRequest(request: Host.AidaClient.CompletionRequest, cursor: number): Promise<{
|
|
274
|
-
suggestionText: string,
|
|
275
|
-
fromCache: boolean,
|
|
276
|
-
citations: Host.AidaClient.Citation[],
|
|
277
|
-
rpcGlobalId?: Host.AidaClient.RpcGlobalId,
|
|
278
|
-
sampleId?: number,
|
|
279
|
-
}|null> {
|
|
280
|
-
const {response, fromCache} = await this.#completeCodeCached(request);
|
|
281
|
-
debugLog('At cursor position', cursor, {request, response, fromCache});
|
|
282
|
-
if (!response) {
|
|
283
|
-
return null;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const suggestionSample = this.#pickSampleFromResponse(response);
|
|
287
|
-
if (!suggestionSample) {
|
|
288
|
-
return null;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const shouldBlock =
|
|
292
|
-
suggestionSample.attributionMetadata?.attributionAction === Host.AidaClient.RecitationAction.BLOCK;
|
|
293
|
-
if (shouldBlock) {
|
|
294
|
-
return null;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const isRepetitive = this.#checkIfSuggestionRepeatsExistingText(suggestionSample.generationString, request);
|
|
298
|
-
if (isRepetitive) {
|
|
299
|
-
return null;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const suggestionText = this.#trimSuggestionOverlap(suggestionSample.generationString, request);
|
|
303
|
-
if (suggestionText.length === 0) {
|
|
304
|
-
return null;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return {
|
|
308
|
-
suggestionText,
|
|
309
|
-
sampleId: suggestionSample.sampleId,
|
|
310
|
-
fromCache,
|
|
311
|
-
citations: suggestionSample.attributionMetadata?.citations ?? [],
|
|
312
|
-
rpcGlobalId: response.metadata.rpcGlobalId,
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
async #requestAidaSuggestion(request: Host.AidaClient.CompletionRequest, cursorPositionAtRequest: number):
|
|
317
|
-
Promise<void> {
|
|
318
|
-
const startTime = performance.now();
|
|
319
|
-
this.dispatchEventToListeners(Events.REQUEST_TRIGGERED, {});
|
|
320
|
-
// Registering AiCodeCompletionRequestTriggered metric even if the request is served from cache
|
|
321
|
-
Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiCodeCompletionRequestTriggered);
|
|
322
|
-
|
|
323
|
-
try {
|
|
324
|
-
const sampleResponse = await this.#generateSampleForRequest(request, cursorPositionAtRequest);
|
|
325
|
-
if (!sampleResponse) {
|
|
326
|
-
this.dispatchEventToListeners(Events.RESPONSE_RECEIVED, {});
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const {
|
|
331
|
-
suggestionText,
|
|
332
|
-
sampleId,
|
|
333
|
-
fromCache,
|
|
334
|
-
citations,
|
|
335
|
-
rpcGlobalId,
|
|
336
|
-
} = sampleResponse;
|
|
337
|
-
const remainingDelay = Math.max(DELAY_BEFORE_SHOWING_RESPONSE_MS - (performance.now() - startTime), 0);
|
|
338
|
-
this.#renderingTimeout = window.setTimeout(() => {
|
|
339
|
-
const currentCursorPosition = this.#callbacks?.getSelectionHead();
|
|
340
|
-
if (currentCursorPosition !== cursorPositionAtRequest) {
|
|
341
|
-
this.dispatchEventToListeners(Events.RESPONSE_RECEIVED, {});
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
this.#callbacks?.setAiAutoCompletion({
|
|
345
|
-
text: suggestionText,
|
|
346
|
-
from: cursorPositionAtRequest,
|
|
347
|
-
rpcGlobalId,
|
|
348
|
-
sampleId,
|
|
349
|
-
startTime,
|
|
350
|
-
onImpression: this.registerUserImpression.bind(this),
|
|
351
|
-
clearCachedRequest: this.clearCachedRequest.bind(this),
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
if (fromCache) {
|
|
355
|
-
Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiCodeCompletionResponseServedFromCache);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
debugLog('Suggestion dispatched to the editor', suggestionText, 'at cursor position', cursorPositionAtRequest);
|
|
359
|
-
this.dispatchEventToListeners(Events.RESPONSE_RECEIVED, {citations});
|
|
360
|
-
}, remainingDelay);
|
|
361
|
-
} catch (e) {
|
|
362
|
-
debugLog('Error while fetching code completion suggestions from AIDA', e);
|
|
363
|
-
this.dispatchEventToListeners(Events.RESPONSE_RECEIVED, {});
|
|
364
|
-
Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiCodeCompletionError);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
225
|
get #userTier(): string|undefined {
|
|
369
226
|
return Root.Runtime.hostConfig.devToolsAiCodeCompletion?.userTier;
|
|
370
227
|
}
|
|
@@ -379,30 +236,6 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
|
|
|
379
236
|
};
|
|
380
237
|
}
|
|
381
238
|
|
|
382
|
-
/**
|
|
383
|
-
* Removes the end of a suggestion if it overlaps with the start of the suffix.
|
|
384
|
-
*/
|
|
385
|
-
#trimSuggestionOverlap(generationString: string, request: Host.AidaClient.CompletionRequest): string {
|
|
386
|
-
const suffix = request.suffix;
|
|
387
|
-
if (!suffix) {
|
|
388
|
-
return generationString;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Iterate from the longest possible overlap down to the shortest
|
|
392
|
-
for (let i = Math.min(generationString.length, suffix.length); i > 0; i--) {
|
|
393
|
-
const overlapCandidate = suffix.substring(0, i);
|
|
394
|
-
if (generationString.endsWith(overlapCandidate)) {
|
|
395
|
-
return generationString.slice(0, -i);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
return generationString;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
#checkIfSuggestionRepeatsExistingText(generationString: string, request: Host.AidaClient.CompletionRequest): boolean {
|
|
402
|
-
const {prefix, suffix} = request;
|
|
403
|
-
return Boolean(prefix.includes(generationString.trim()) || suffix?.includes(generationString.trim()));
|
|
404
|
-
}
|
|
405
|
-
|
|
406
239
|
#checkCachedRequestForResponse(request: Host.AidaClient.CompletionRequest): Host.AidaClient.CompletionResponse|null {
|
|
407
240
|
if (!this.#aidaRequestCache || this.#aidaRequestCache.request.suffix !== request.suffix ||
|
|
408
241
|
JSON.stringify(this.#aidaRequestCache.request.options) !== JSON.stringify(request.options)) {
|
|
@@ -476,12 +309,6 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
|
|
|
476
309
|
this.#aidaRequestCache = undefined;
|
|
477
310
|
}
|
|
478
311
|
|
|
479
|
-
onTextChanged(
|
|
480
|
-
prefix: string, suffix: string, cursorPositionAtRequest: number,
|
|
481
|
-
inferenceLanguage?: Host.AidaClient.AidaInferenceLanguage): void {
|
|
482
|
-
this.#debouncedRequestAidaSuggestion(prefix, suffix, cursorPositionAtRequest, inferenceLanguage);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
312
|
async completeCode(
|
|
486
313
|
prefix: string, suffix: string, cursorPositionAtRequest: number,
|
|
487
314
|
inferenceLanguage?: Host.AidaClient.AidaInferenceLanguage,
|
|
@@ -525,18 +352,3 @@ export const enum ContextFlavor {
|
|
|
525
352
|
CONSOLE = 'console', // generated code can contain console specific APIs like `$0`.
|
|
526
353
|
SOURCES = 'sources',
|
|
527
354
|
}
|
|
528
|
-
|
|
529
|
-
export const enum Events {
|
|
530
|
-
RESPONSE_RECEIVED = 'ResponseReceived',
|
|
531
|
-
REQUEST_TRIGGERED = 'RequestTriggered',
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
export interface ResponseReceivedEvent {
|
|
535
|
-
citations?: Host.AidaClient.Citation[];
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
export interface EventTypes {
|
|
539
|
-
[Events.RESPONSE_RECEIVED]: ResponseReceivedEvent;
|
|
540
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
541
|
-
[Events.REQUEST_TRIGGERED]: {};
|
|
542
|
-
}
|
|
@@ -134,6 +134,7 @@ export interface ViewInput {
|
|
|
134
134
|
onTakeScreenshot: () => void;
|
|
135
135
|
onRemoveImageInput: () => void;
|
|
136
136
|
onImageUpload: (ev: Event) => void;
|
|
137
|
+
onImagePaste: (event: ClipboardEvent) => void;
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
export type ViewOutput = undefined;
|
|
@@ -287,6 +288,7 @@ export const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLE
|
|
|
287
288
|
wrap="hard"
|
|
288
289
|
maxlength="10000"
|
|
289
290
|
@keydown=${input.onTextAreaKeyDown}
|
|
291
|
+
@paste=${input.onImagePaste}
|
|
290
292
|
@input=${(event: KeyboardEvent) => {
|
|
291
293
|
input.onTextInputChange((event.target as HTMLInputElement).value);
|
|
292
294
|
}}
|
|
@@ -548,6 +550,25 @@ export class ChatInput extends UI.Widget.Widget implements SDK.TargetManager.Obs
|
|
|
548
550
|
});
|
|
549
551
|
}
|
|
550
552
|
|
|
553
|
+
#handleImagePaste = (event: ClipboardEvent): void => {
|
|
554
|
+
if (this.conversationType !== AiAssistanceModel.AiHistoryStorage.ConversationType.STYLING) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const files = event.clipboardData?.files;
|
|
559
|
+
if (!files || files.length === 0) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const imageFile = Array.from(files).find(file => file.type.startsWith('image/'));
|
|
564
|
+
if (!imageFile) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
event.preventDefault();
|
|
569
|
+
void this.#handleLoadImage(imageFile);
|
|
570
|
+
};
|
|
571
|
+
|
|
551
572
|
async #handleLoadImage(file: File): Promise<void> {
|
|
552
573
|
const showLoadingTimeout = setTimeout(() => {
|
|
553
574
|
this.#imageInput = {isLoading: true};
|
|
@@ -631,6 +652,7 @@ export class ChatInput extends UI.Widget.Widget implements SDK.TargetManager.Obs
|
|
|
631
652
|
textAreaRef: this.#textAreaRef,
|
|
632
653
|
onContextClick: this.onContextClick,
|
|
633
654
|
onInspectElementClick: this.onInspectElementClick,
|
|
655
|
+
onImagePaste: this.#handleImagePaste,
|
|
634
656
|
onNewConversation: this.onNewConversation,
|
|
635
657
|
onTextInputChange: () => {
|
|
636
658
|
this.requestUpdate();
|
|
@@ -8,6 +8,7 @@ import '../../ui/components/tooltips/tooltips.js';
|
|
|
8
8
|
import * as Host from '../../core/host/host.js';
|
|
9
9
|
import * as i18n from '../../core/i18n/i18n.js';
|
|
10
10
|
import * as Root from '../../core/root/root.js';
|
|
11
|
+
import * as AiCodeCompletion from '../../models/ai_code_completion/ai_code_completion.js';
|
|
11
12
|
import * as UI from '../../ui/legacy/legacy.js';
|
|
12
13
|
import {Directives, html, nothing, render} from '../../ui/lit/lit.js';
|
|
13
14
|
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
|
|
@@ -26,13 +27,23 @@ const UIStringsNotTranslate = {
|
|
|
26
27
|
/**
|
|
27
28
|
* @description Text for tooltip shown on hovering over "Relevant Data" in the disclaimer text for AI code completion.
|
|
28
29
|
*/
|
|
29
|
-
|
|
30
|
+
tooltipDisclaimerTextForAiCodeCompletionInConsole:
|
|
30
31
|
'To generate code suggestions, your console input and the history of your current console session are shared with Google. This data may be seen by human reviewers to improve this feature.',
|
|
31
32
|
/**
|
|
32
33
|
* @description Text for tooltip shown on hovering over "Relevant Data" in the disclaimer text for AI code completion.
|
|
33
34
|
*/
|
|
34
|
-
|
|
35
|
-
'To generate code suggestions, your console input and the history of your current console session are shared with Google. This data will not be used to improve Google’s AI models.',
|
|
35
|
+
tooltipDisclaimerTextForAiCodeCompletionNoLoggingInConsole:
|
|
36
|
+
'To generate code suggestions, your console input and the history of your current console session are shared with Google. This data will not be used to improve Google’s AI models. Your organization may change these settings at any time.',
|
|
37
|
+
/**
|
|
38
|
+
* @description Text for tooltip shown on hovering over "Relevant Data" in the disclaimer text for AI code generation in Sources panel.
|
|
39
|
+
*/
|
|
40
|
+
tooltipDisclaimerTextForAiCodeCompletionInSources:
|
|
41
|
+
'To generate code suggestions, the contents of the currently open file are shared with Google. This data may be seen by human reviewers to improve this feature.',
|
|
42
|
+
/**
|
|
43
|
+
* @description Text for tooltip shown on hovering over "Relevant Data" in the disclaimer text for AI code generation in Sources panel.
|
|
44
|
+
*/
|
|
45
|
+
tooltipDisclaimerTextForAiCodeCompletionNoLoggingInSources:
|
|
46
|
+
'To generate code suggestions, the contents of the currently open file are shared with Google. This data will not be used to improve Google’s AI models. Your organization may change these settings at any time.',
|
|
36
47
|
/**
|
|
37
48
|
* Text for tooltip shown on hovering over spinner.
|
|
38
49
|
*/
|
|
@@ -49,12 +60,27 @@ const UIStringsNotTranslate = {
|
|
|
49
60
|
|
|
50
61
|
const lockedString = i18n.i18n.lockedString;
|
|
51
62
|
|
|
63
|
+
function getTooltipDisclaimerText(noLogging: boolean, panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor): string {
|
|
64
|
+
switch (panel) {
|
|
65
|
+
case AiCodeCompletion.AiCodeCompletion.ContextFlavor.CONSOLE:
|
|
66
|
+
return noLogging ?
|
|
67
|
+
lockedString(UIStringsNotTranslate.tooltipDisclaimerTextForAiCodeCompletionNoLoggingInConsole) :
|
|
68
|
+
lockedString(UIStringsNotTranslate.tooltipDisclaimerTextForAiCodeCompletionInConsole);
|
|
69
|
+
case AiCodeCompletion.AiCodeCompletion.ContextFlavor.SOURCES:
|
|
70
|
+
return noLogging ?
|
|
71
|
+
lockedString(UIStringsNotTranslate.tooltipDisclaimerTextForAiCodeCompletionNoLoggingInSources) :
|
|
72
|
+
lockedString(UIStringsNotTranslate.tooltipDisclaimerTextForAiCodeCompletionInSources);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
52
76
|
export interface ViewInput {
|
|
53
77
|
disclaimerTooltipId?: string;
|
|
54
78
|
spinnerTooltipId?: string;
|
|
55
79
|
noLogging: boolean;
|
|
56
80
|
aidaAvailability?: Host.AidaClient.AidaAccessPreconditions;
|
|
57
81
|
onManageInSettingsTooltipClick: () => void;
|
|
82
|
+
// TODO(b/472268298): Remove ContextFlavor explicitly and pass required values
|
|
83
|
+
panel?: AiCodeCompletion.AiCodeCompletion.ContextFlavor;
|
|
58
84
|
}
|
|
59
85
|
|
|
60
86
|
export interface ViewOutput {
|
|
@@ -67,10 +93,11 @@ export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) =
|
|
|
67
93
|
export const DEFAULT_SUMMARY_TOOLBAR_VIEW: View =
|
|
68
94
|
(input, output, target) => {
|
|
69
95
|
if (input.aidaAvailability !== Host.AidaClient.AidaAccessPreconditions.AVAILABLE || !input.disclaimerTooltipId ||
|
|
70
|
-
!input.spinnerTooltipId) {
|
|
96
|
+
!input.spinnerTooltipId || !input.panel) {
|
|
71
97
|
render(nothing, target);
|
|
72
98
|
return;
|
|
73
99
|
}
|
|
100
|
+
const tooltipDisclaimerText = getTooltipDisclaimerText(input.noLogging, input.panel);
|
|
74
101
|
// clang-format off
|
|
75
102
|
render(
|
|
76
103
|
html`
|
|
@@ -118,7 +145,7 @@ export const DEFAULT_SUMMARY_TOOLBAR_VIEW: View =
|
|
|
118
145
|
}
|
|
119
146
|
})}>
|
|
120
147
|
<div class="disclaimer-tooltip-container"><div class="tooltip-text">
|
|
121
|
-
${
|
|
148
|
+
${tooltipDisclaimerText}
|
|
122
149
|
</div>
|
|
123
150
|
<span
|
|
124
151
|
tabIndex="0"
|
|
@@ -146,6 +173,7 @@ export class AiCodeCompletionDisclaimer extends UI.Widget.Widget {
|
|
|
146
173
|
#loading = false;
|
|
147
174
|
#loadingStartTime = 0;
|
|
148
175
|
#spinnerLoadingTimeout: number|undefined;
|
|
176
|
+
#panel?: AiCodeCompletion.AiCodeCompletion.ContextFlavor;
|
|
149
177
|
|
|
150
178
|
#aidaAvailability?: Host.AidaClient.AidaAccessPreconditions;
|
|
151
179
|
#boundOnAidaAvailabilityChange: () => Promise<void>;
|
|
@@ -196,6 +224,11 @@ export class AiCodeCompletionDisclaimer extends UI.Widget.Widget {
|
|
|
196
224
|
}
|
|
197
225
|
}
|
|
198
226
|
|
|
227
|
+
set panel(panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor) {
|
|
228
|
+
this.#panel = panel;
|
|
229
|
+
this.requestUpdate();
|
|
230
|
+
}
|
|
231
|
+
|
|
199
232
|
async #onAidaAvailabilityChange(): Promise<void> {
|
|
200
233
|
const currentAidaAvailability = await Host.AidaClient.AidaClient.checkAccessPreconditions();
|
|
201
234
|
if (currentAidaAvailability !== this.#aidaAvailability) {
|
|
@@ -217,6 +250,7 @@ export class AiCodeCompletionDisclaimer extends UI.Widget.Widget {
|
|
|
217
250
|
noLogging: this.#noLogging,
|
|
218
251
|
aidaAvailability: this.#aidaAvailability,
|
|
219
252
|
onManageInSettingsTooltipClick: this.#onManageInSettingsTooltipClick.bind(this),
|
|
253
|
+
panel: this.#panel,
|
|
220
254
|
},
|
|
221
255
|
this.#viewOutput, this.contentElement);
|
|
222
256
|
}
|
|
@@ -7,6 +7,7 @@ import '../../ui/components/tooltips/tooltips.js';
|
|
|
7
7
|
|
|
8
8
|
import * as Host from '../../core/host/host.js';
|
|
9
9
|
import * as i18n from '../../core/i18n/i18n.js';
|
|
10
|
+
import type * as AiCodeCompletion from '../../models/ai_code_completion/ai_code_completion.js';
|
|
10
11
|
import * as UI from '../../ui/legacy/legacy.js';
|
|
11
12
|
import {Directives, html, nothing, render} from '../../ui/lit/lit.js';
|
|
12
13
|
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
|
|
@@ -32,6 +33,7 @@ export interface AiCodeCompletionSummaryToolbarProps {
|
|
|
32
33
|
disclaimerTooltipId?: string;
|
|
33
34
|
spinnerTooltipId?: string;
|
|
34
35
|
hasTopBorder?: boolean;
|
|
36
|
+
panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export interface ViewInput {
|
|
@@ -42,6 +44,7 @@ export interface ViewInput {
|
|
|
42
44
|
loading: boolean;
|
|
43
45
|
hasTopBorder: boolean;
|
|
44
46
|
aidaAvailability?: Host.AidaClient.AidaAccessPreconditions;
|
|
47
|
+
panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
export type View = (input: ViewInput, output: undefined, target: HTMLElement) => void;
|
|
@@ -65,6 +68,7 @@ export const DEFAULT_SUMMARY_TOOLBAR_VIEW: View = (input, _output, target) => {
|
|
|
65
68
|
disclaimerTooltipId: input.disclaimerTooltipId,
|
|
66
69
|
spinnerTooltipId: input.spinnerTooltipId,
|
|
67
70
|
loading: input.loading,
|
|
71
|
+
panel: input.panel,
|
|
68
72
|
})} class="disclaimer-widget"></devtools-widget>` : nothing;
|
|
69
73
|
|
|
70
74
|
const recitationNotice = input.citations && input.citations.size > 0 ?
|
|
@@ -110,6 +114,7 @@ export class AiCodeCompletionSummaryToolbar extends UI.Widget.Widget {
|
|
|
110
114
|
#citations = new Set<string>();
|
|
111
115
|
#loading = false;
|
|
112
116
|
#hasTopBorder = false;
|
|
117
|
+
#panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor;
|
|
113
118
|
|
|
114
119
|
#aidaAvailability?: Host.AidaClient.AidaAccessPreconditions;
|
|
115
120
|
#boundOnAidaAvailabilityChange: () => Promise<void>;
|
|
@@ -120,6 +125,7 @@ export class AiCodeCompletionSummaryToolbar extends UI.Widget.Widget {
|
|
|
120
125
|
this.#spinnerTooltipId = props.spinnerTooltipId;
|
|
121
126
|
this.#citationsTooltipId = props.citationsTooltipId;
|
|
122
127
|
this.#hasTopBorder = props.hasTopBorder ?? false;
|
|
128
|
+
this.#panel = props.panel;
|
|
123
129
|
this.#boundOnAidaAvailabilityChange = this.#onAidaAvailabilityChange.bind(this);
|
|
124
130
|
this.#view = view ?? DEFAULT_SUMMARY_TOOLBAR_VIEW;
|
|
125
131
|
this.requestUpdate();
|
|
@@ -158,6 +164,7 @@ export class AiCodeCompletionSummaryToolbar extends UI.Widget.Widget {
|
|
|
158
164
|
loading: this.#loading,
|
|
159
165
|
hasTopBorder: this.#hasTopBorder,
|
|
160
166
|
aidaAvailability: this.#aidaAvailability,
|
|
167
|
+
panel: this.#panel,
|
|
161
168
|
},
|
|
162
169
|
undefined, this.contentElement);
|
|
163
170
|
}
|
|
@@ -24,13 +24,21 @@ const UIStringsNotTranslate = {
|
|
|
24
24
|
*/
|
|
25
25
|
cmdItoGenerateCode: 'Cmd+I to generate code',
|
|
26
26
|
/**
|
|
27
|
-
* Text for teaser when generating suggestion.
|
|
27
|
+
* @description Text for teaser when generating suggestion.
|
|
28
28
|
*/
|
|
29
29
|
generating: 'Generating... (esc to cancel)',
|
|
30
30
|
/**
|
|
31
|
-
* Text for teaser for discoverability.
|
|
31
|
+
* @description Text for teaser for discoverability.
|
|
32
32
|
*/
|
|
33
33
|
writeACommentToGenerateCode: 'Write a comment to generate code',
|
|
34
|
+
/**
|
|
35
|
+
* @description Text for teaser when suggestion has been generated.
|
|
36
|
+
*/
|
|
37
|
+
tab: 'tab',
|
|
38
|
+
/**
|
|
39
|
+
* @description Text for teaser when suggestion has been generated.
|
|
40
|
+
*/
|
|
41
|
+
toAccept: 'to accept',
|
|
34
42
|
/**
|
|
35
43
|
* @description Text for tooltip shown on hovering over "Relevant Data" in the disclaimer text for AI code generation in Console panel.
|
|
36
44
|
*/
|
|
@@ -68,6 +76,7 @@ export enum AiCodeGenerationTeaserDisplayState {
|
|
|
68
76
|
TRIGGER = 'trigger',
|
|
69
77
|
DISCOVERY = 'discovery',
|
|
70
78
|
LOADING = 'loading',
|
|
79
|
+
GENERATED = 'generated',
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
function getTooltipDisclaimerText(noLogging: boolean, panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor): string {
|
|
@@ -94,6 +103,7 @@ export interface ViewInput {
|
|
|
94
103
|
|
|
95
104
|
export interface ViewOutput {
|
|
96
105
|
hideTooltip?: () => void;
|
|
106
|
+
setTimerText?: (text: string) => void;
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void;
|
|
@@ -165,7 +175,27 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
|
|
|
165
175
|
}
|
|
166
176
|
|
|
167
177
|
case AiCodeGenerationTeaserDisplayState.LOADING: {
|
|
168
|
-
|
|
178
|
+
// clang-format off
|
|
179
|
+
teaserLabel = html`
|
|
180
|
+
<span class="ai-code-generation-spinner"></span> ${lockedString(UIStringsNotTranslate.generating)}
|
|
181
|
+
<span class="ai-code-generation-timer" ${Directives.ref(el => {
|
|
182
|
+
if (el) {
|
|
183
|
+
output.setTimerText = (text: string) => {
|
|
184
|
+
el.textContent = text;
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
})}></span>`;
|
|
188
|
+
// clang-format on
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case AiCodeGenerationTeaserDisplayState.GENERATED: {
|
|
193
|
+
// clang-format off
|
|
194
|
+
teaserLabel = html`<div class="ai-code-generation-teaser-generated">
|
|
195
|
+
<span>${lockedString(UIStringsNotTranslate.tab)}</span>
|
|
196
|
+
${lockedString(UIStringsNotTranslate.toAccept)}
|
|
197
|
+
</div>`;
|
|
198
|
+
// clang-format on
|
|
169
199
|
break;
|
|
170
200
|
}
|
|
171
201
|
}
|
|
@@ -192,6 +222,8 @@ export class AiCodeGenerationTeaser extends UI.Widget.Widget {
|
|
|
192
222
|
#disclaimerTooltipId?: string;
|
|
193
223
|
#noLogging: boolean; // Whether the enterprise setting is `ALLOW_WITHOUT_LOGGING` or not.
|
|
194
224
|
#panel?: AiCodeCompletion.AiCodeCompletion.ContextFlavor;
|
|
225
|
+
#timerIntervalId?: number;
|
|
226
|
+
#loadStartTime?: number;
|
|
195
227
|
|
|
196
228
|
constructor(view?: View) {
|
|
197
229
|
super();
|
|
@@ -214,6 +246,11 @@ export class AiCodeGenerationTeaser extends UI.Widget.Widget {
|
|
|
214
246
|
this.#viewOutput, this.contentElement);
|
|
215
247
|
}
|
|
216
248
|
|
|
249
|
+
override willHide(): void {
|
|
250
|
+
super.willHide();
|
|
251
|
+
this.#stopLoadingAnimation();
|
|
252
|
+
}
|
|
253
|
+
|
|
217
254
|
get displayState(): AiCodeGenerationTeaserDisplayState {
|
|
218
255
|
return this.#displayState;
|
|
219
256
|
}
|
|
@@ -224,6 +261,36 @@ export class AiCodeGenerationTeaser extends UI.Widget.Widget {
|
|
|
224
261
|
}
|
|
225
262
|
this.#displayState = displayState;
|
|
226
263
|
this.requestUpdate();
|
|
264
|
+
if (this.#displayState === AiCodeGenerationTeaserDisplayState.LOADING) {
|
|
265
|
+
// wait update to complete so that setTimerText has been set properly
|
|
266
|
+
void this.updateComplete.then(() => {
|
|
267
|
+
void this.#startLoadingAnimation();
|
|
268
|
+
});
|
|
269
|
+
} else if (this.#loadStartTime) {
|
|
270
|
+
this.#stopLoadingAnimation();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
#startLoadingAnimation(): void {
|
|
275
|
+
this.#stopLoadingAnimation();
|
|
276
|
+
this.#loadStartTime = performance.now();
|
|
277
|
+
|
|
278
|
+
this.#viewOutput.setTimerText?.('(0s)');
|
|
279
|
+
|
|
280
|
+
this.#timerIntervalId = window.setInterval(() => {
|
|
281
|
+
if (this.#loadStartTime) {
|
|
282
|
+
const elapsedSeconds = Math.floor((performance.now() - this.#loadStartTime) / 1000);
|
|
283
|
+
this.#viewOutput.setTimerText?.(`(${elapsedSeconds}s)`);
|
|
284
|
+
}
|
|
285
|
+
}, 1000);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#stopLoadingAnimation(): void {
|
|
289
|
+
if (this.#timerIntervalId) {
|
|
290
|
+
clearInterval(this.#timerIntervalId);
|
|
291
|
+
this.#timerIntervalId = undefined;
|
|
292
|
+
}
|
|
293
|
+
this.#loadStartTime = undefined;
|
|
227
294
|
}
|
|
228
295
|
|
|
229
296
|
set disclaimerTooltipId(disclaimerTooltipId: string) {
|
|
@@ -16,6 +16,18 @@
|
|
|
16
16
|
align-items: center;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
.ai-code-generation-teaser-generated {
|
|
20
|
+
display: inline-flex;
|
|
21
|
+
gap: var(--sys-size-2);
|
|
22
|
+
color: var(--sys-color-primary);
|
|
23
|
+
|
|
24
|
+
span {
|
|
25
|
+
border: var(--sys-size-1) solid var(--sys-color-primary);
|
|
26
|
+
border-radius: var(--sys-shape-corner-extra-small);
|
|
27
|
+
padding: 0 var(--sys-size-3);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
.new-badge {
|
|
20
32
|
font-style: normal;
|
|
21
33
|
display: inline-block;
|
|
@@ -44,5 +56,23 @@
|
|
|
44
56
|
}
|
|
45
57
|
}
|
|
46
58
|
}
|
|
59
|
+
|
|
60
|
+
.ai-code-generation-spinner::before {
|
|
61
|
+
content: "⠋";
|
|
62
|
+
animation: teaser-spinner-animation 1s linear infinite;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@keyframes teaser-spinner-animation {
|
|
67
|
+
0% { content: "⠋"; }
|
|
68
|
+
10% { content: "⠙"; }
|
|
69
|
+
20% { content: "⠹"; }
|
|
70
|
+
30% { content: "⠸"; }
|
|
71
|
+
40% { content: "⠼"; }
|
|
72
|
+
50% { content: "⠴"; }
|
|
73
|
+
60% { content: "⠦"; }
|
|
74
|
+
70% { content: "⠧"; }
|
|
75
|
+
80% { content: "⠇"; }
|
|
76
|
+
90% { content: "⠏"; }
|
|
47
77
|
}
|
|
48
78
|
}
|
|
@@ -649,7 +649,8 @@ export class ConsoleView extends UI.Widget.VBox implements
|
|
|
649
649
|
this.aiCodeCompletionSummaryToolbar = new AiCodeCompletionSummaryToolbar({
|
|
650
650
|
citationsTooltipId: CITATIONS_TOOLTIP_ID,
|
|
651
651
|
disclaimerTooltipId: DISCLAIMER_TOOLTIP_ID,
|
|
652
|
-
spinnerTooltipId: SPINNER_TOOLTIP_ID
|
|
652
|
+
spinnerTooltipId: SPINNER_TOOLTIP_ID,
|
|
653
|
+
panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor.CONSOLE,
|
|
653
654
|
});
|
|
654
655
|
this.aiCodeCompletionSummaryToolbarContainer =
|
|
655
656
|
this.element.createChild('div', 'ai-code-completion-summary-toolbar-container');
|
|
@@ -102,6 +102,7 @@ export class AiCodeCompletionPlugin extends Plugin {
|
|
|
102
102
|
this.#aiCodeCompletionDisclaimer = new PanelCommon.AiCodeCompletionDisclaimer();
|
|
103
103
|
this.#aiCodeCompletionDisclaimer.disclaimerTooltipId = DISCLAIMER_TOOLTIP_ID;
|
|
104
104
|
this.#aiCodeCompletionDisclaimer.spinnerTooltipId = SPINNER_TOOLTIP_ID;
|
|
105
|
+
this.#aiCodeCompletionDisclaimer.panel = AiCodeCompletion.AiCodeCompletion.ContextFlavor.SOURCES;
|
|
105
106
|
this.#aiCodeCompletionDisclaimer.show(this.#aiCodeCompletionDisclaimerContainer, undefined, true);
|
|
106
107
|
}
|
|
107
108
|
|
|
@@ -109,8 +110,11 @@ export class AiCodeCompletionPlugin extends Plugin {
|
|
|
109
110
|
if (this.#aiCodeCompletionCitationsToolbar) {
|
|
110
111
|
return;
|
|
111
112
|
}
|
|
112
|
-
this.#aiCodeCompletionCitationsToolbar =
|
|
113
|
-
|
|
113
|
+
this.#aiCodeCompletionCitationsToolbar = new PanelCommon.AiCodeCompletionSummaryToolbar({
|
|
114
|
+
citationsTooltipId: CITATIONS_TOOLTIP_ID,
|
|
115
|
+
hasTopBorder: true,
|
|
116
|
+
panel: AiCodeCompletion.AiCodeCompletion.ContextFlavor.SOURCES
|
|
117
|
+
});
|
|
114
118
|
this.#aiCodeCompletionCitationsToolbar.show(this.#aiCodeCompletionCitationsToolbarContainer, undefined, true);
|
|
115
119
|
}
|
|
116
120
|
|
|
@@ -122,21 +122,25 @@ export class AiCodeCompletionProvider {
|
|
|
122
122
|
this.#aiCodeCompletion?.clearCachedRequest();
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
// TODO(b/445394511): Update setup and cleanup method so that config callbacks are not
|
|
126
|
-
// called twice.
|
|
127
125
|
#setupAiCodeCompletion(): void {
|
|
128
126
|
if (!this.#editor || !this.#aiCodeCompletionConfig) {
|
|
129
127
|
return;
|
|
130
128
|
}
|
|
131
|
-
if (
|
|
132
|
-
this
|
|
133
|
-
|
|
134
|
-
this.#aiCodeCompletionConfig.completionContext.stopSequences);
|
|
129
|
+
if (this.#aiCodeCompletion) {
|
|
130
|
+
// early return as this means that code completion was previously setup
|
|
131
|
+
return;
|
|
135
132
|
}
|
|
133
|
+
this.#aiCodeCompletion = new AiCodeCompletion.AiCodeCompletion.AiCodeCompletion(
|
|
134
|
+
{aidaClient: this.#aidaClient}, this.#aiCodeCompletionConfig.panel, undefined,
|
|
135
|
+
this.#aiCodeCompletionConfig.completionContext.stopSequences);
|
|
136
136
|
this.#aiCodeCompletionConfig.onFeatureEnabled();
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
#cleanupAiCodeCompletion(): void {
|
|
140
|
+
if (!this.#aiCodeCompletion) {
|
|
141
|
+
// early return as this means there is no code completion to clean up
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
140
144
|
if (this.#suggestionRenderingTimeout) {
|
|
141
145
|
clearTimeout(this.#suggestionRenderingTimeout);
|
|
142
146
|
this.#suggestionRenderingTimeout = undefined;
|
|
@@ -208,6 +212,21 @@ export class AiCodeCompletionProvider {
|
|
|
208
212
|
this.#editor?.editor.dispatch({effects: this.#teaserCompartment.reconfigure([])});
|
|
209
213
|
}
|
|
210
214
|
|
|
215
|
+
/**
|
|
216
|
+
* This method is responsible for fetching code completion suggestions and
|
|
217
|
+
* displaying them in the text editor.
|
|
218
|
+
*
|
|
219
|
+
* 1. **Debouncing requests:** As the user types, we don't want to send a request
|
|
220
|
+
* for every keystroke. Instead, we use debouncing to schedule a request
|
|
221
|
+
* only after the user has paused typing for a short period
|
|
222
|
+
* (AIDA_REQUEST_THROTTLER_TIMEOUT_MS). This prevents spamming the backend with
|
|
223
|
+
* requests for intermediate typing states.
|
|
224
|
+
*
|
|
225
|
+
* 2. **Delaying suggestions:** When a suggestion is received from the AIDA
|
|
226
|
+
* backend, we don't show it immediately. There is a minimum delay
|
|
227
|
+
* (DELAY_BEFORE_SHOWING_RESPONSE_MS) from when the request was sent to when
|
|
228
|
+
* the suggestion is displayed.
|
|
229
|
+
*/
|
|
211
230
|
#triggerAiCodeCompletion(update: CodeMirror.ViewUpdate): void {
|
|
212
231
|
if (!update.docChanged || !this.#editor || !this.#aiCodeCompletion) {
|
|
213
232
|
return;
|
|
@@ -287,8 +306,7 @@ export class AiCodeCompletionProvider {
|
|
|
287
306
|
citations,
|
|
288
307
|
rpcGlobalId,
|
|
289
308
|
} = sampleResponse;
|
|
290
|
-
const remainingDelay = Math.max(
|
|
291
|
-
AiCodeCompletion.AiCodeCompletion.DELAY_BEFORE_SHOWING_RESPONSE_MS - (performance.now() - startTime), 0);
|
|
309
|
+
const remainingDelay = Math.max(DELAY_BEFORE_SHOWING_RESPONSE_MS - (performance.now() - startTime), 0);
|
|
292
310
|
this.#suggestionRenderingTimeout = window.setTimeout(() => {
|
|
293
311
|
const currentCursorPosition = this.#editor?.editor.state.selection.main.head;
|
|
294
312
|
if (currentCursorPosition !== cursorPositionAtRequest) {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Copyright 2025 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 CodeMirror from '../../../third_party/codemirror.next/codemirror.next.js';
|
|
6
|
+
|
|
7
|
+
const LINE_COMMENT_PATTERN = /^(?:\/\/|#)\s*/;
|
|
8
|
+
const BLOCK_COMMENT_START_PATTERN = /^\/\*+\s*/;
|
|
9
|
+
const BLOCK_COMMENT_END_PATTERN = /\s*\*+\/$/;
|
|
10
|
+
const BLOCK_COMMENT_LINE_PREFIX_PATTERN = /^\s*\*\s?/;
|
|
11
|
+
|
|
12
|
+
function findLastNonWhitespacePos(state: CodeMirror.EditorState, cursorPosition: number): number {
|
|
13
|
+
const line = state.doc.lineAt(cursorPosition);
|
|
14
|
+
const textBefore = line.text.substring(0, cursorPosition - line.from);
|
|
15
|
+
const effectiveEnd = line.from + textBefore.trimEnd().length;
|
|
16
|
+
return effectiveEnd;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveCommentNode(state: CodeMirror.EditorState, cursorPosition: number): CodeMirror.SyntaxNode|undefined {
|
|
20
|
+
const tree = CodeMirror.syntaxTree(state);
|
|
21
|
+
const lookupPos = findLastNonWhitespacePos(state, cursorPosition);
|
|
22
|
+
// Find the innermost syntax node at the last non-whitespace character position.
|
|
23
|
+
// The bias of -1 makes it check the character to the left of the position.
|
|
24
|
+
const node = tree.resolveInner(lookupPos, -1);
|
|
25
|
+
const nodeType = node.type.name;
|
|
26
|
+
// Check if the node type is a comment AND the cursor is within the node's range.
|
|
27
|
+
if (nodeType.includes('Comment') && cursorPosition >= node.to) {
|
|
28
|
+
if (!nodeType.includes('BlockComment')) {
|
|
29
|
+
return node;
|
|
30
|
+
}
|
|
31
|
+
// An unclosed block comment can result in the parser inserting an error.
|
|
32
|
+
let hasInternalError = false;
|
|
33
|
+
tree.iterate({
|
|
34
|
+
from: node.from,
|
|
35
|
+
to: node.to,
|
|
36
|
+
enter: n => {
|
|
37
|
+
if (n.type.isError) {
|
|
38
|
+
hasInternalError = true;
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
return hasInternalError ? undefined : node;
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function extractBlockComment(rawText: string): string|undefined {
|
|
50
|
+
// Remove /* and */, whitespace, and common leading asterisks on new lines
|
|
51
|
+
let cleaned = rawText.replace(BLOCK_COMMENT_START_PATTERN, '').replace(BLOCK_COMMENT_END_PATTERN, '');
|
|
52
|
+
// Remove leading " * " from multi-line block comments
|
|
53
|
+
cleaned = cleaned.split('\n').map(line => line.replace(BLOCK_COMMENT_LINE_PREFIX_PATTERN, '')).join('\n').trim();
|
|
54
|
+
return cleaned;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function extractLineComment(rawText: string): string {
|
|
58
|
+
return rawText.replace(LINE_COMMENT_PATTERN, '').trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class AiCodeGenerationParser {
|
|
62
|
+
static extractCommentText(state: CodeMirror.EditorState, cursorPosition: number): string|undefined {
|
|
63
|
+
const node = resolveCommentNode(state, cursorPosition);
|
|
64
|
+
if (!node) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const nodeType = node.type.name;
|
|
68
|
+
const rawText = state.doc.sliceString(node.from, node.to);
|
|
69
|
+
if (nodeType.includes('LineComment')) {
|
|
70
|
+
return extractLineComment(rawText);
|
|
71
|
+
}
|
|
72
|
+
if (nodeType.includes('BlockComment')) {
|
|
73
|
+
return extractBlockComment(rawText);
|
|
74
|
+
}
|
|
75
|
+
return rawText;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -13,6 +13,7 @@ import * as UI from '../../../ui/legacy/legacy.js';
|
|
|
13
13
|
import * as VisualLogging from '../../visual_logging/visual_logging.js';
|
|
14
14
|
|
|
15
15
|
import {AiCodeCompletionTeaserPlaceholder} from './AiCodeCompletionTeaserPlaceholder.js';
|
|
16
|
+
import {AiCodeGenerationParser} from './AiCodeGenerationParser.js';
|
|
16
17
|
import {
|
|
17
18
|
acceptAiAutoCompleteSuggestion,
|
|
18
19
|
aiAutoCompleteSuggestion,
|
|
@@ -79,11 +80,11 @@ export class AiCodeGenerationProvider {
|
|
|
79
80
|
extension(): CodeMirror.Extension[] {
|
|
80
81
|
return [
|
|
81
82
|
CodeMirror.EditorView.updateListener.of(update => this.#activateTeaser(update)),
|
|
82
|
-
CodeMirror.EditorView.updateListener.of(update => this.#
|
|
83
|
+
CodeMirror.EditorView.updateListener.of(update => this.#abortOrDismissGenerationDuringUpdate(update)),
|
|
83
84
|
aiAutoCompleteSuggestion,
|
|
84
85
|
aiAutoCompleteSuggestionState,
|
|
85
86
|
aiCodeGenerationTeaserModeState,
|
|
86
|
-
this.#generationTeaserCompartment.of([]),
|
|
87
|
+
CodeMirror.Prec.highest(this.#generationTeaserCompartment.of([])),
|
|
87
88
|
CodeMirror.Prec.highest(CodeMirror.keymap.of(this.#editorKeymap())),
|
|
88
89
|
];
|
|
89
90
|
}
|
|
@@ -142,9 +143,7 @@ export class AiCodeGenerationProvider {
|
|
|
142
143
|
return false;
|
|
143
144
|
}
|
|
144
145
|
if (hasActiveAiSuggestion(this.#editor.state)) {
|
|
145
|
-
this.#
|
|
146
|
-
effects: setAiAutoCompleteSuggestion.of(null),
|
|
147
|
-
});
|
|
146
|
+
this.#dismissTeaserAndSuggestion();
|
|
148
147
|
return true;
|
|
149
148
|
}
|
|
150
149
|
const generationTeaserIsLoading = this.#generationTeaser.displayState ===
|
|
@@ -152,7 +151,7 @@ export class AiCodeGenerationProvider {
|
|
|
152
151
|
if (this.#generationTeaser.isShowing() && generationTeaserIsLoading) {
|
|
153
152
|
this.#controller.abort();
|
|
154
153
|
this.#controller = new AbortController();
|
|
155
|
-
this.#
|
|
154
|
+
this.#dismissTeaserAndSuggestion();
|
|
156
155
|
return true;
|
|
157
156
|
}
|
|
158
157
|
return false;
|
|
@@ -194,9 +193,14 @@ export class AiCodeGenerationProvider {
|
|
|
194
193
|
];
|
|
195
194
|
}
|
|
196
195
|
|
|
197
|
-
#
|
|
196
|
+
#dismissTeaserAndSuggestion(): void {
|
|
198
197
|
this.#generationTeaser.displayState = PanelCommon.AiCodeGenerationTeaser.AiCodeGenerationTeaserDisplayState.TRIGGER;
|
|
199
|
-
this.#editor?.dispatch({
|
|
198
|
+
this.#editor?.dispatch({
|
|
199
|
+
effects: [
|
|
200
|
+
setAiCodeGenerationTeaserMode.of(AiCodeGenerationTeaserMode.DISMISSED),
|
|
201
|
+
setAiAutoCompleteSuggestion.of(null),
|
|
202
|
+
]
|
|
203
|
+
});
|
|
200
204
|
}
|
|
201
205
|
|
|
202
206
|
#activateTeaser(update: CodeMirror.ViewUpdate): void {
|
|
@@ -211,26 +215,35 @@ export class AiCodeGenerationProvider {
|
|
|
211
215
|
}
|
|
212
216
|
|
|
213
217
|
/**
|
|
214
|
-
* Monitors editor changes to cancel an ongoing AI generation
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
218
|
+
* Monitors editor changes to cancel an ongoing AI generation or dismiss one
|
|
219
|
+
* if it already exists.
|
|
220
|
+
* We abort the request (or dismiss suggestion) and dismiss the teaser if the
|
|
221
|
+
* user modifies the document or moves their cursor/selection. These actions
|
|
222
|
+
* indicate the user is no longer focused on the current generation point or
|
|
223
|
+
* has manually resumed editing, making the suggestion irrelevant.
|
|
219
224
|
*/
|
|
220
|
-
#
|
|
225
|
+
#abortOrDismissGenerationDuringUpdate(update: CodeMirror.ViewUpdate): void {
|
|
221
226
|
if (!update.docChanged && update.state.selection.main.head === update.startState.selection.main.head) {
|
|
222
227
|
return;
|
|
223
228
|
}
|
|
224
229
|
const currentTeaserMode = update.state.field(aiCodeGenerationTeaserModeState);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (
|
|
230
|
+
if (currentTeaserMode === AiCodeGenerationTeaserMode.DISMISSED) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (this.#generationTeaser.displayState ===
|
|
234
|
+
PanelCommon.AiCodeGenerationTeaser.AiCodeGenerationTeaserDisplayState.LOADING) {
|
|
235
|
+
this.#controller.abort();
|
|
236
|
+
this.#controller = new AbortController();
|
|
237
|
+
this.#dismissTeaserAndSuggestion();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (this.#generationTeaser.displayState ===
|
|
241
|
+
PanelCommon.AiCodeGenerationTeaser.AiCodeGenerationTeaserDisplayState.GENERATED) {
|
|
242
|
+
update.view.dispatch({effects: setAiAutoCompleteSuggestion.of(null)});
|
|
243
|
+
this.#generationTeaser.displayState =
|
|
244
|
+
PanelCommon.AiCodeGenerationTeaser.AiCodeGenerationTeaserDisplayState.TRIGGER;
|
|
229
245
|
return;
|
|
230
246
|
}
|
|
231
|
-
this.#controller.abort();
|
|
232
|
-
this.#controller = new AbortController();
|
|
233
|
-
this.#dismissTeaser();
|
|
234
247
|
}
|
|
235
248
|
|
|
236
249
|
async #triggerAiCodeGeneration(options?: {signal?: AbortSignal}): Promise<void> {
|
|
@@ -240,9 +253,8 @@ export class AiCodeGenerationProvider {
|
|
|
240
253
|
|
|
241
254
|
this.#generationTeaser.displayState = PanelCommon.AiCodeGenerationTeaser.AiCodeGenerationTeaserDisplayState.LOADING;
|
|
242
255
|
const cursor = this.#editor.state.selection.main.head;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (query.trim().length === 0) {
|
|
256
|
+
const query = AiCodeGenerationParser.extractCommentText(this.#editor.state, cursor);
|
|
257
|
+
if (!query || query.trim().length === 0) {
|
|
246
258
|
return;
|
|
247
259
|
}
|
|
248
260
|
|
|
@@ -256,7 +268,7 @@ export class AiCodeGenerationProvider {
|
|
|
256
268
|
this.#aiCodeGenerationConfig?.generationContext.inferenceLanguage, options);
|
|
257
269
|
|
|
258
270
|
if (this.#generationTeaser) {
|
|
259
|
-
this.#
|
|
271
|
+
this.#dismissTeaserAndSuggestion();
|
|
260
272
|
}
|
|
261
273
|
|
|
262
274
|
if (!generationResponse || generationResponse.samples.length === 0) {
|
|
@@ -275,19 +287,25 @@ export class AiCodeGenerationProvider {
|
|
|
275
287
|
const suggestionText = matchArray ? matchArray[1].trim() : topSample.generationString;
|
|
276
288
|
|
|
277
289
|
this.#editor.dispatch({
|
|
278
|
-
effects:
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
290
|
+
effects: [
|
|
291
|
+
setAiAutoCompleteSuggestion.of({
|
|
292
|
+
text: '\n' + suggestionText,
|
|
293
|
+
from: cursor,
|
|
294
|
+
rpcGlobalId: generationResponse.metadata.rpcGlobalId,
|
|
295
|
+
sampleId: topSample.sampleId,
|
|
296
|
+
startTime,
|
|
297
|
+
onImpression: this.#aiCodeGeneration?.registerUserImpression.bind(this.#aiCodeGeneration),
|
|
298
|
+
}),
|
|
299
|
+
setAiCodeGenerationTeaserMode.of(AiCodeGenerationTeaserMode.ACTIVE)
|
|
300
|
+
]
|
|
286
301
|
});
|
|
302
|
+
this.#generationTeaser.displayState =
|
|
303
|
+
PanelCommon.AiCodeGenerationTeaser.AiCodeGenerationTeaserDisplayState.GENERATED;
|
|
287
304
|
|
|
288
305
|
AiCodeGeneration.debugLog('Suggestion dispatched to the editor', suggestionText);
|
|
289
306
|
const citations = topSample.attributionMetadata?.citations ?? [];
|
|
290
307
|
this.#aiCodeGenerationConfig?.onResponseReceived(citations);
|
|
308
|
+
return;
|
|
291
309
|
} catch (e) {
|
|
292
310
|
AiCodeGeneration.debugLog('Error while fetching code generation suggestions from AIDA', e);
|
|
293
311
|
this.#aiCodeGenerationConfig?.onResponseReceived([]);
|
|
@@ -295,7 +313,7 @@ export class AiCodeGenerationProvider {
|
|
|
295
313
|
}
|
|
296
314
|
|
|
297
315
|
if (this.#generationTeaser) {
|
|
298
|
-
this.#
|
|
316
|
+
this.#dismissTeaserAndSuggestion();
|
|
299
317
|
}
|
|
300
318
|
}
|
|
301
319
|
}
|
|
@@ -327,8 +345,7 @@ function aiCodeGenerationTeaserExtension(teaser: PanelCommon.AiCodeGenerationTea
|
|
|
327
345
|
const line = this.#view.state.doc.lineAt(cursorPosition);
|
|
328
346
|
|
|
329
347
|
const isEmptyLine = line.length === 0;
|
|
330
|
-
|
|
331
|
-
const isComment = line.text.startsWith('//');
|
|
348
|
+
const isComment = Boolean(AiCodeGenerationParser.extractCommentText(this.#view.state, cursorPosition));
|
|
332
349
|
const isCursorAtEndOfLine = cursorPosition >= line.to;
|
|
333
350
|
|
|
334
351
|
if ((isEmptyLine) || (isComment && isCursorAtEndOfLine)) {
|
|
@@ -341,9 +358,10 @@ function aiCodeGenerationTeaserExtension(teaser: PanelCommon.AiCodeGenerationTea
|
|
|
341
358
|
}
|
|
342
359
|
|
|
343
360
|
#updateTeaserState(state: CodeMirror.EditorState): void {
|
|
344
|
-
// Only handle non loading states, as updates during generation are handled by
|
|
345
|
-
// #
|
|
346
|
-
if (teaser.displayState === PanelCommon.AiCodeGenerationTeaser.AiCodeGenerationTeaserDisplayState.LOADING
|
|
361
|
+
// Only handle non loading and non generated states, as updates during and after generation are handled by
|
|
362
|
+
// #abortOrDismissGenerationDuringUpdate in AiCodeGenerationProvider
|
|
363
|
+
if (teaser.displayState === PanelCommon.AiCodeGenerationTeaser.AiCodeGenerationTeaserDisplayState.LOADING ||
|
|
364
|
+
teaser.displayState === PanelCommon.AiCodeGenerationTeaser.AiCodeGenerationTeaserDisplayState.GENERATED) {
|
|
347
365
|
return;
|
|
348
366
|
}
|
|
349
367
|
const cursorPosition = state.selection.main.head;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
export * as AiCodeCompletionProvider from './AiCodeCompletionProvider.js';
|
|
6
6
|
export * as AiCodeCompletionTeaserPlaceholder from './AiCodeCompletionTeaserPlaceholder.js';
|
|
7
|
+
export * as AiCodeGenerationParser from './AiCodeGenerationParser.js';
|
|
7
8
|
export * as AiCodeGenerationProvider from './AiCodeGenerationProvider.js';
|
|
8
9
|
export * as AutocompleteHistory from './AutocompleteHistory.js';
|
|
9
10
|
export * as Config from './config.js';
|
package/package.json
CHANGED