chrome-devtools-frontend 1.0.1510848 → 1.0.1512349
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/Images/src/ai-explorer-badge.svg +114 -0
- package/front_end/Images/src/code-whisperer-badge.svg +166 -0
- package/front_end/Images/src/devtools-user-badge.svg +129 -0
- package/front_end/Images/src/dom-detective-badge.svg +136 -0
- package/front_end/Images/src/speedster-badge.svg +166 -0
- package/front_end/core/host/AidaClient.ts +2 -0
- package/front_end/core/host/GdpClient.ts +38 -2
- package/front_end/core/i18n/NumberFormatter.ts +7 -0
- package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +3 -19
- package/front_end/models/ai_assistance/ai_assistance.ts +1 -1
- package/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.ts +7 -6
- package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.snapshot.txt +119 -119
- package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.ts +43 -52
- package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.snapshot.txt +100 -100
- package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.ts +12 -18
- package/front_end/models/ai_assistance/data_formatters/UnitFormatters.ts +151 -0
- package/front_end/models/ai_code_completion/AiCodeCompletion.ts +3 -0
- package/front_end/models/badges/Badge.ts +7 -4
- package/front_end/models/badges/DOMDetectiveBadge.ts +20 -0
- package/front_end/models/badges/SpeedsterBadge.ts +4 -1
- package/front_end/models/badges/StarterBadge.ts +5 -1
- package/front_end/models/badges/UserBadges.ts +33 -7
- package/front_end/models/trace/ModelImpl.ts +0 -13
- package/front_end/models/trace/insights/Common.ts +19 -0
- package/front_end/panels/common/AiCodeCompletionDisclaimer.ts +36 -9
- package/front_end/panels/common/AiCodeCompletionSummaryToolbar.ts +32 -0
- package/front_end/panels/common/AiCodeCompletionTeaser.ts +14 -2
- package/front_end/panels/common/BadgeNotification.ts +119 -9
- package/front_end/panels/common/badgeNotification.css +4 -0
- package/front_end/panels/console/ConsolePrompt.ts +26 -0
- package/front_end/panels/elements/ElementsTreeElement.ts +12 -0
- package/front_end/panels/elements/ElementsTreeOutline.ts +3 -0
- package/front_end/panels/elements/StylePropertiesSection.ts +3 -0
- package/front_end/panels/elements/StylePropertyTreeElement.ts +5 -0
- package/front_end/panels/settings/SettingsScreen.ts +3 -9
- package/front_end/panels/settings/components/SyncSection.ts +6 -2
- package/front_end/panels/sources/AiCodeCompletionPlugin.ts +35 -6
- package/front_end/panels/timeline/TimelinePanel.ts +22 -10
- package/front_end/panels/timeline/TimelineUIUtils.ts +4 -3
- package/front_end/panels/timeline/utils/InsightAIContext.ts +0 -19
- package/front_end/ui/legacy/filter.css +1 -1
- package/front_end/ui/legacy/inspectorCommon.css +1 -1
- package/front_end/ui/legacy/softDropDownButton.css +1 -1
- package/package.json +1 -1
- package/front_end/models/ai_assistance/data_formatters/Types.ts +0 -9
@@ -0,0 +1,151 @@
|
|
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
|
+
/**
|
6
|
+
* This module contains unit formatters that are only to be used within
|
7
|
+
* the AI models because they do not account for locales other than en-US.
|
8
|
+
*/
|
9
|
+
const defaultTimeFormatterOptions: Intl.NumberFormatOptions = {
|
10
|
+
style: 'unit',
|
11
|
+
unitDisplay: 'narrow',
|
12
|
+
minimumFractionDigits: 0,
|
13
|
+
maximumFractionDigits: 1,
|
14
|
+
} as const;
|
15
|
+
|
16
|
+
const defaultByteFormatterOptions: Intl.NumberFormatOptions = {
|
17
|
+
style: 'unit',
|
18
|
+
unitDisplay: 'narrow',
|
19
|
+
minimumFractionDigits: 0,
|
20
|
+
maximumFractionDigits: 1,
|
21
|
+
} as const;
|
22
|
+
|
23
|
+
const timeFormatters = {
|
24
|
+
milli: new Intl.NumberFormat('en-US', {
|
25
|
+
...defaultTimeFormatterOptions,
|
26
|
+
unit: 'millisecond',
|
27
|
+
}),
|
28
|
+
second: new Intl.NumberFormat('en-US', {
|
29
|
+
...defaultTimeFormatterOptions,
|
30
|
+
unit: 'second',
|
31
|
+
}),
|
32
|
+
micro: new Intl.NumberFormat('en-US', {
|
33
|
+
...defaultTimeFormatterOptions,
|
34
|
+
unit: 'microsecond',
|
35
|
+
}),
|
36
|
+
} as const;
|
37
|
+
|
38
|
+
const byteFormatters = {
|
39
|
+
bytes: new Intl.NumberFormat('en-US', {
|
40
|
+
...defaultByteFormatterOptions,
|
41
|
+
// Don't need as much precision on bytes.
|
42
|
+
minimumFractionDigits: 0,
|
43
|
+
maximumFractionDigits: 0,
|
44
|
+
unit: 'byte',
|
45
|
+
}),
|
46
|
+
kilobytes: new Intl.NumberFormat('en-US', {
|
47
|
+
...defaultByteFormatterOptions,
|
48
|
+
unit: 'kilobyte',
|
49
|
+
}),
|
50
|
+
megabytes: new Intl.NumberFormat('en-US', {
|
51
|
+
...defaultByteFormatterOptions,
|
52
|
+
unit: 'megabyte',
|
53
|
+
}),
|
54
|
+
} as const;
|
55
|
+
|
56
|
+
function numberIsTooLarge(x: number): boolean {
|
57
|
+
return !Number.isFinite(x) || x === Number.MAX_VALUE;
|
58
|
+
}
|
59
|
+
|
60
|
+
export function seconds(x: number): string {
|
61
|
+
if (numberIsTooLarge(x)) {
|
62
|
+
return '-';
|
63
|
+
}
|
64
|
+
if (x === 0) {
|
65
|
+
return formatAndEnsureSpace(timeFormatters.second, x);
|
66
|
+
}
|
67
|
+
|
68
|
+
const asMilli = x * 1_000;
|
69
|
+
|
70
|
+
if (asMilli < 1) {
|
71
|
+
return micros(x * 1_000_000);
|
72
|
+
}
|
73
|
+
|
74
|
+
if (asMilli < 1_000) {
|
75
|
+
return millis(asMilli);
|
76
|
+
}
|
77
|
+
return formatAndEnsureSpace(timeFormatters.second, x);
|
78
|
+
}
|
79
|
+
|
80
|
+
export function millis(x: number): string {
|
81
|
+
if (numberIsTooLarge(x)) {
|
82
|
+
return '-';
|
83
|
+
}
|
84
|
+
return formatAndEnsureSpace(timeFormatters.milli, x);
|
85
|
+
}
|
86
|
+
|
87
|
+
export function micros(x: number): string {
|
88
|
+
if (numberIsTooLarge(x)) {
|
89
|
+
return '-';
|
90
|
+
}
|
91
|
+
|
92
|
+
if (x < 100) {
|
93
|
+
return formatAndEnsureSpace(timeFormatters.micro, x);
|
94
|
+
}
|
95
|
+
|
96
|
+
const asMilli = x / 1_000;
|
97
|
+
return millis(asMilli);
|
98
|
+
}
|
99
|
+
|
100
|
+
export function bytes(x: number): string {
|
101
|
+
if (x < 1_000) {
|
102
|
+
return formatAndEnsureSpace(byteFormatters.bytes, x);
|
103
|
+
}
|
104
|
+
const kilobytes = x / 1_000;
|
105
|
+
if (kilobytes < 1_000) {
|
106
|
+
return formatAndEnsureSpace(byteFormatters.kilobytes, kilobytes);
|
107
|
+
}
|
108
|
+
|
109
|
+
const megabytes = kilobytes / 1_000;
|
110
|
+
return formatAndEnsureSpace(byteFormatters.megabytes, megabytes);
|
111
|
+
}
|
112
|
+
|
113
|
+
/**
|
114
|
+
* When using 'narrow' unitDisplay, many locales exclude the space between the literal and the unit.
|
115
|
+
* We don't like that, so when there is no space literal we inject the provided separator manually.
|
116
|
+
*/
|
117
|
+
function formatAndEnsureSpace(formatter: Intl.NumberFormat, value: number, separator = '\xA0'): string {
|
118
|
+
const parts = formatter.formatToParts(value);
|
119
|
+
|
120
|
+
let hasSpace = false;
|
121
|
+
for (const part of parts) {
|
122
|
+
if (part.type === 'literal') {
|
123
|
+
if (part.value === ' ') {
|
124
|
+
hasSpace = true;
|
125
|
+
part.value = separator;
|
126
|
+
} else if (part.value === separator) {
|
127
|
+
hasSpace = true;
|
128
|
+
}
|
129
|
+
}
|
130
|
+
}
|
131
|
+
|
132
|
+
if (hasSpace) {
|
133
|
+
return parts.map(part => part.value).join('');
|
134
|
+
}
|
135
|
+
|
136
|
+
const unitIndex = parts.findIndex(part => part.type === 'unit');
|
137
|
+
|
138
|
+
// Unexpected for there to be no unit, but just in case, handle that.
|
139
|
+
if (unitIndex === -1) {
|
140
|
+
return parts.map(part => part.value).join('');
|
141
|
+
}
|
142
|
+
|
143
|
+
// For locales where the unit comes first (sw), the space has to come after the unit.
|
144
|
+
if (unitIndex === 0) {
|
145
|
+
return parts[0].value + separator + parts.slice(1).map(part => part.value).join('');
|
146
|
+
}
|
147
|
+
|
148
|
+
// Otherwise, it comes before.
|
149
|
+
return parts.slice(0, unitIndex).map(part => part.value).join('') + separator +
|
150
|
+
parts.slice(unitIndex).map(part => part.value).join('');
|
151
|
+
}
|
@@ -377,6 +377,9 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
|
|
377
377
|
clearTimeout(this.#renderingTimeout);
|
378
378
|
this.#renderingTimeout = undefined;
|
379
379
|
}
|
380
|
+
this.#editor.dispatch({
|
381
|
+
effects: TextEditor.Config.setAiAutoCompleteSuggestion.of(null),
|
382
|
+
});
|
380
383
|
}
|
381
384
|
}
|
382
385
|
|
@@ -6,29 +6,32 @@ import * as Common from '../../core/common/common.js';
|
|
6
6
|
|
7
7
|
export enum BadgeAction {
|
8
8
|
CSS_RULE_MODIFIED = 'css-rule-modified',
|
9
|
+
DOM_ELEMENT_OR_ATTRIBUTE_EDITED = 'dom-element-or-attribute-edited',
|
10
|
+
MODERN_DOM_BADGE_CLICKED = 'modern-dom-badge-clicked',
|
9
11
|
PERFORMANCE_INSIGHT_CLICKED = 'performance-insight-clicked',
|
10
12
|
}
|
11
13
|
|
12
14
|
export type BadgeActionEvents = Record<BadgeAction, void>;
|
13
15
|
|
14
16
|
export interface BadgeContext {
|
15
|
-
|
17
|
+
onTriggerBadge: (badge: Badge) => void;
|
16
18
|
badgeActionEventTarget: Common.ObjectWrapper.ObjectWrapper<BadgeActionEvents>;
|
17
19
|
}
|
18
20
|
|
19
21
|
export abstract class Badge {
|
20
|
-
#
|
22
|
+
#onTriggerBadge: (badge: Badge) => void;
|
21
23
|
#badgeActionEventTarget: Common.ObjectWrapper.ObjectWrapper<BadgeActionEvents>;
|
22
24
|
#eventListeners: Common.EventTarget.EventDescriptor[] = [];
|
23
25
|
#triggeredBefore = false;
|
24
26
|
|
25
27
|
abstract readonly name: string;
|
26
28
|
abstract readonly title: string;
|
29
|
+
abstract readonly imageUri: string;
|
27
30
|
abstract readonly interestedActions: readonly BadgeAction[];
|
28
31
|
readonly isStarterBadge: boolean = false;
|
29
32
|
|
30
33
|
constructor(context: BadgeContext) {
|
31
|
-
this.#
|
34
|
+
this.#onTriggerBadge = context.onTriggerBadge;
|
32
35
|
this.#badgeActionEventTarget = context.badgeActionEventTarget;
|
33
36
|
}
|
34
37
|
|
@@ -40,7 +43,7 @@ export abstract class Badge {
|
|
40
43
|
|
41
44
|
this.#triggeredBefore = true;
|
42
45
|
this.deactivate();
|
43
|
-
this.#
|
46
|
+
this.#onTriggerBadge(this);
|
44
47
|
}
|
45
48
|
|
46
49
|
activate(): void {
|
@@ -0,0 +1,20 @@
|
|
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 {Badge, BadgeAction} from './Badge.js';
|
6
|
+
|
7
|
+
const DOM_DETECTIVE_BADGE_IMAGE_URI = new URL('../../Images/gdp-logo-standalone.svg', import.meta.url).toString();
|
8
|
+
export class DOMDetectiveBadge extends Badge {
|
9
|
+
override readonly name = 'awards/dom-detective-badge';
|
10
|
+
override readonly title = 'DOM Detective';
|
11
|
+
override readonly imageUri = DOM_DETECTIVE_BADGE_IMAGE_URI;
|
12
|
+
|
13
|
+
override readonly interestedActions = [
|
14
|
+
BadgeAction.DOM_ELEMENT_OR_ATTRIBUTE_EDITED,
|
15
|
+
] as const;
|
16
|
+
|
17
|
+
handleAction(_action: BadgeAction): void {
|
18
|
+
this.trigger();
|
19
|
+
}
|
20
|
+
}
|
@@ -4,10 +4,13 @@
|
|
4
4
|
|
5
5
|
import {Badge, BadgeAction} from './Badge.js';
|
6
6
|
|
7
|
+
const SPEEDSTER_BADGE_URI = new URL('../../Images/gdp-logo-standalone.svg', import.meta.url).toString();
|
7
8
|
export class SpeedsterBadge extends Badge {
|
8
|
-
|
9
|
+
// TODO(ergunsh): Update the name to be the actual badge for DevTools.
|
10
|
+
override readonly name = 'profiles/me/awards/developers.google.com%2Fprofile%2Fbadges%2Flegacy%2Ftest';
|
9
11
|
override readonly title = 'Speedster';
|
10
12
|
override readonly interestedActions = [BadgeAction.PERFORMANCE_INSIGHT_CLICKED] as const;
|
13
|
+
override readonly imageUri = SPEEDSTER_BADGE_URI;
|
11
14
|
|
12
15
|
handleAction(_action: BadgeAction): void {
|
13
16
|
this.trigger();
|
@@ -4,14 +4,18 @@
|
|
4
4
|
|
5
5
|
import {Badge, BadgeAction} from './Badge.js';
|
6
6
|
|
7
|
+
const STARTER_BADGE_IMAGE_URI = new URL('../../Images/gdp-logo-standalone.svg', import.meta.url).toString();
|
7
8
|
export class StarterBadge extends Badge {
|
8
9
|
override readonly isStarterBadge = true;
|
9
|
-
|
10
|
+
// TODO(ergunsh): Update the name to be the actual badge for DevTools.
|
11
|
+
override readonly name = 'profiles/me/awards/developers.google.com%2Fprofile%2Fbadges%2Fprofile%2Fcreated-profile';
|
10
12
|
override readonly title = 'Chrome DevTools User';
|
13
|
+
override readonly imageUri = STARTER_BADGE_IMAGE_URI;
|
11
14
|
|
12
15
|
// TODO(ergunsh): Add remaining non-trivial event definitions
|
13
16
|
override readonly interestedActions = [
|
14
17
|
BadgeAction.CSS_RULE_MODIFIED,
|
18
|
+
BadgeAction.DOM_ELEMENT_OR_ATTRIBUTE_EDITED,
|
15
19
|
] as const;
|
16
20
|
|
17
21
|
handleAction(_action: BadgeAction): void {
|
@@ -6,6 +6,7 @@ import * as Common from '../../core/common/common.js';
|
|
6
6
|
import * as Host from '../../core/host/host.js';
|
7
7
|
|
8
8
|
import type {Badge, BadgeAction, BadgeActionEvents, BadgeContext} from './Badge.js';
|
9
|
+
import {DOMDetectiveBadge} from './DOMDetectiveBadge.js';
|
9
10
|
import {SpeedsterBadge} from './SpeedsterBadge.js';
|
10
11
|
import {StarterBadge} from './StarterBadge.js';
|
11
12
|
|
@@ -23,12 +24,13 @@ let userBadgesInstance: UserBadges|undefined = undefined;
|
|
23
24
|
export class UserBadges extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
|
24
25
|
readonly #badgeActionEventTarget = new Common.ObjectWrapper.ObjectWrapper<BadgeActionEvents>();
|
25
26
|
|
26
|
-
#receiveBadgesSetting
|
27
|
+
#receiveBadgesSetting: Common.Settings.Setting<Boolean>;
|
27
28
|
#allBadges: Badge[];
|
28
29
|
|
29
30
|
static readonly BADGE_REGISTRY: BadgeClass[] = [
|
30
31
|
StarterBadge,
|
31
32
|
SpeedsterBadge,
|
33
|
+
DOMDetectiveBadge,
|
32
34
|
];
|
33
35
|
|
34
36
|
private constructor() {
|
@@ -37,11 +39,10 @@ export class UserBadges extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
|
|
37
39
|
this.#receiveBadgesSetting = Common.Settings.Settings.instance().moduleSetting('receive-gdp-badges');
|
38
40
|
this.#receiveBadgesSetting.addChangeListener(this.#reconcileBadges, this);
|
39
41
|
|
40
|
-
this.#allBadges =
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
}));
|
42
|
+
this.#allBadges = UserBadges.BADGE_REGISTRY.map(badgeCtor => new badgeCtor({
|
43
|
+
onTriggerBadge: this.#onTriggerBadge.bind(this),
|
44
|
+
badgeActionEventTarget: this.#badgeActionEventTarget,
|
45
|
+
}));
|
45
46
|
}
|
46
47
|
|
47
48
|
static instance({forceNew}: {forceNew: boolean} = {forceNew: false}): UserBadges {
|
@@ -65,7 +66,28 @@ export class UserBadges extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
|
|
65
66
|
this.#badgeActionEventTarget.dispatchEventToListeners(action);
|
66
67
|
}
|
67
68
|
|
68
|
-
#
|
69
|
+
async #onTriggerBadge(badge: Badge): Promise<void> {
|
70
|
+
let shouldAwardBadge = false;
|
71
|
+
// By default, we award non-starter badges directly when they are triggered.
|
72
|
+
if (!badge.isStarterBadge) {
|
73
|
+
shouldAwardBadge = true;
|
74
|
+
} else {
|
75
|
+
const gdpProfile = await Host.GdpClient.GdpClient.instance().getProfile();
|
76
|
+
const receiveBadgesSettingEnabled = Boolean(this.#receiveBadgesSetting.get());
|
77
|
+
// If there is a GDP profile and the user has enabled receiving badges, we award the starter badge as well.
|
78
|
+
if (gdpProfile && receiveBadgesSettingEnabled) {
|
79
|
+
shouldAwardBadge = true;
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
// Awarding was needed and not successful, we don't show the notification
|
84
|
+
if (shouldAwardBadge) {
|
85
|
+
const result = await Host.GdpClient.GdpClient.instance().createAward({name: badge.name});
|
86
|
+
if (!result) {
|
87
|
+
return;
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
69
91
|
this.dispatchEventToListeners(Events.BADGE_TRIGGERED, badge);
|
70
92
|
}
|
71
93
|
|
@@ -115,4 +137,8 @@ export class UserBadges extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
|
|
115
137
|
|
116
138
|
reconcileBadgesFinishedForTest(): void {
|
117
139
|
}
|
140
|
+
|
141
|
+
isReceiveBadgesSettingEnabled(): boolean {
|
142
|
+
return Boolean(this.#receiveBadgesSetting?.get());
|
143
|
+
}
|
118
144
|
}
|
@@ -118,19 +118,6 @@ export class Model extends EventTarget {
|
|
118
118
|
metadata,
|
119
119
|
resolveSourceMap: config?.resolveSourceMap,
|
120
120
|
};
|
121
|
-
if (!parseConfig.logger &&
|
122
|
-
(window.location.href.includes('devtools/bundled') || window.location.search.includes('debugFrontend'))) {
|
123
|
-
// Someone is debugging DevTools, enable the logger.
|
124
|
-
const times: Record<string, number> = {};
|
125
|
-
parseConfig.logger = {
|
126
|
-
start(id) {
|
127
|
-
times[id] = performance.now();
|
128
|
-
},
|
129
|
-
end(id) {
|
130
|
-
performance.measure(id, {start: times[id]});
|
131
|
-
},
|
132
|
-
};
|
133
|
-
}
|
134
121
|
await this.#processor.parse(traceEvents, parseConfig);
|
135
122
|
this.#storeParsedFileData(file, this.#processor.parsedTrace, this.#processor.insights);
|
136
123
|
// We only push the file onto this.#traces here once we know it's valid
|
@@ -12,6 +12,7 @@ import * as Types from '../types/types.js';
|
|
12
12
|
import {getLogNormalScore} from './Statistics.js';
|
13
13
|
import {
|
14
14
|
InsightKeys,
|
15
|
+
type InsightModel,
|
15
16
|
type InsightModels,
|
16
17
|
type InsightSet,
|
17
18
|
type InsightSetContext,
|
@@ -432,3 +433,21 @@ export function calculateDocFirstByteTs(docRequest: Types.Events.SyntheticNetwor
|
|
432
433
|
Helpers.Timing.secondsToMicro(timing.requestTime) +
|
433
434
|
Helpers.Timing.milliToMicro(timing.receiveHeadersStart ?? timing.receiveHeadersEnd));
|
434
435
|
}
|
436
|
+
|
437
|
+
/**
|
438
|
+
* Calculates the trace bounds for the given insight that are relevant.
|
439
|
+
*
|
440
|
+
* Uses the insight's overlays to determine the relevant trace bounds. If there are
|
441
|
+
* no overlays, falls back to the insight set's navigation bounds.
|
442
|
+
*/
|
443
|
+
export function insightBounds(
|
444
|
+
insight: InsightModel, insightSetBounds: Types.Timing.TraceWindowMicro): Types.Timing.TraceWindowMicro {
|
445
|
+
const overlays = insight.createOverlays?.() ?? [];
|
446
|
+
const windows = overlays.map(Helpers.Timing.traceWindowFromOverlay).filter(bounds => !!bounds);
|
447
|
+
const overlaysBounds = Helpers.Timing.combineTraceWindowsMicro(windows);
|
448
|
+
if (overlaysBounds) {
|
449
|
+
return overlaysBounds;
|
450
|
+
}
|
451
|
+
|
452
|
+
return insightSetBounds;
|
453
|
+
}
|
@@ -5,6 +5,7 @@
|
|
5
5
|
import '../../ui/components/spinners/spinners.js';
|
6
6
|
import '../../ui/components/tooltips/tooltips.js';
|
7
7
|
|
8
|
+
import * as Host from '../../core/host/host.js';
|
8
9
|
import * as i18n from '../../core/i18n/i18n.js';
|
9
10
|
import * as Root from '../../core/root/root.js';
|
10
11
|
import * as UI from '../../ui/legacy/legacy.js';
|
@@ -47,6 +48,7 @@ const lockedString = i18n.i18n.lockedString;
|
|
47
48
|
export interface ViewInput {
|
48
49
|
disclaimerTooltipId?: string;
|
49
50
|
noLogging: boolean;
|
51
|
+
aidaAvailability?: Host.AidaClient.AidaAccessPreconditions;
|
50
52
|
onManageInSettingsTooltipClick: () => void;
|
51
53
|
}
|
52
54
|
|
@@ -57,13 +59,12 @@ export interface ViewOutput {
|
|
57
59
|
|
58
60
|
export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void;
|
59
61
|
|
60
|
-
export const DEFAULT_SUMMARY_TOOLBAR_VIEW: View =
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
// clang-format off
|
62
|
+
export const DEFAULT_SUMMARY_TOOLBAR_VIEW: View = (input, output, target) => {
|
63
|
+
if (input.aidaAvailability !== Host.AidaClient.AidaAccessPreconditions.AVAILABLE || !input.disclaimerTooltipId) {
|
64
|
+
render(nothing, target);
|
65
|
+
return;
|
66
|
+
}
|
67
|
+
// clang-format off
|
67
68
|
render(
|
68
69
|
html`
|
69
70
|
<style>${styles}</style>
|
@@ -114,8 +115,8 @@ export const DEFAULT_SUMMARY_TOOLBAR_VIEW: View =
|
|
114
115
|
>${lockedString(UIStringsNotTranslate.manageInSettings)}</span></div></devtools-tooltip>
|
115
116
|
</div>
|
116
117
|
`, target);
|
117
|
-
|
118
|
-
|
118
|
+
// clang-format on
|
119
|
+
};
|
119
120
|
|
120
121
|
const MINIMUM_LOADING_STATE_TIMEOUT = 1000;
|
121
122
|
|
@@ -129,11 +130,15 @@ export class AiCodeCompletionDisclaimer extends UI.Widget.Widget {
|
|
129
130
|
#loadingStartTime = 0;
|
130
131
|
#spinnerLoadingTimeout: number|undefined;
|
131
132
|
|
133
|
+
#aidaAvailability?: Host.AidaClient.AidaAccessPreconditions;
|
134
|
+
#boundOnAidaAvailabilityChange: () => Promise<void>;
|
135
|
+
|
132
136
|
constructor(element?: HTMLElement, view: View = DEFAULT_SUMMARY_TOOLBAR_VIEW) {
|
133
137
|
super(element);
|
134
138
|
this.markAsExternallyManaged();
|
135
139
|
this.#noLogging = Root.Runtime.hostConfig.aidaAvailability?.enterprisePolicyValue ===
|
136
140
|
Root.Runtime.GenAiEnterprisePolicyValue.ALLOW_WITHOUT_LOGGING;
|
141
|
+
this.#boundOnAidaAvailabilityChange = this.#onAidaAvailabilityChange.bind(this);
|
137
142
|
this.#view = view;
|
138
143
|
}
|
139
144
|
|
@@ -169,6 +174,14 @@ export class AiCodeCompletionDisclaimer extends UI.Widget.Widget {
|
|
169
174
|
}
|
170
175
|
}
|
171
176
|
|
177
|
+
async #onAidaAvailabilityChange(): Promise<void> {
|
178
|
+
const currentAidaAvailability = await Host.AidaClient.AidaClient.checkAccessPreconditions();
|
179
|
+
if (currentAidaAvailability !== this.#aidaAvailability) {
|
180
|
+
this.#aidaAvailability = currentAidaAvailability;
|
181
|
+
this.requestUpdate();
|
182
|
+
}
|
183
|
+
}
|
184
|
+
|
172
185
|
#onManageInSettingsTooltipClick(): void {
|
173
186
|
this.#viewOutput.hideTooltip?.();
|
174
187
|
void UI.ViewManager.ViewManager.instance().showView('chrome-ai');
|
@@ -179,8 +192,22 @@ export class AiCodeCompletionDisclaimer extends UI.Widget.Widget {
|
|
179
192
|
{
|
180
193
|
disclaimerTooltipId: this.#disclaimerTooltipId,
|
181
194
|
noLogging: this.#noLogging,
|
195
|
+
aidaAvailability: this.#aidaAvailability,
|
182
196
|
onManageInSettingsTooltipClick: this.#onManageInSettingsTooltipClick.bind(this),
|
183
197
|
},
|
184
198
|
this.#viewOutput, this.contentElement);
|
185
199
|
}
|
200
|
+
|
201
|
+
override wasShown(): void {
|
202
|
+
super.wasShown();
|
203
|
+
Host.AidaClient.HostConfigTracker.instance().addEventListener(
|
204
|
+
Host.AidaClient.Events.AIDA_AVAILABILITY_CHANGED, this.#boundOnAidaAvailabilityChange);
|
205
|
+
void this.#onAidaAvailabilityChange();
|
206
|
+
}
|
207
|
+
|
208
|
+
override willHide(): void {
|
209
|
+
super.willHide();
|
210
|
+
Host.AidaClient.HostConfigTracker.instance().removeEventListener(
|
211
|
+
Host.AidaClient.Events.AIDA_AVAILABILITY_CHANGED, this.#boundOnAidaAvailabilityChange);
|
212
|
+
}
|
186
213
|
}
|
@@ -5,6 +5,7 @@
|
|
5
5
|
import '../../ui/components/spinners/spinners.js';
|
6
6
|
import '../../ui/components/tooltips/tooltips.js';
|
7
7
|
|
8
|
+
import * as Host from '../../core/host/host.js';
|
8
9
|
import * as i18n from '../../core/i18n/i18n.js';
|
9
10
|
import * as UI from '../../ui/legacy/legacy.js';
|
10
11
|
import {Directives, html, nothing, render} from '../../ui/lit/lit.js';
|
@@ -38,11 +39,16 @@ export interface ViewInput {
|
|
38
39
|
citationsTooltipId: string;
|
39
40
|
loading: boolean;
|
40
41
|
hasTopBorder: boolean;
|
42
|
+
aidaAvailability?: Host.AidaClient.AidaAccessPreconditions;
|
41
43
|
}
|
42
44
|
|
43
45
|
export type View = (input: ViewInput, output: undefined, target: HTMLElement) => void;
|
44
46
|
|
45
47
|
export const DEFAULT_SUMMARY_TOOLBAR_VIEW: View = (input, _output, target) => {
|
48
|
+
if (input.aidaAvailability !== Host.AidaClient.AidaAccessPreconditions.AVAILABLE) {
|
49
|
+
render(nothing, target);
|
50
|
+
return;
|
51
|
+
}
|
46
52
|
const toolbarClasses = Directives.classMap({
|
47
53
|
'ai-code-completion-summary-toolbar': true,
|
48
54
|
'has-disclaimer': Boolean(input.disclaimerTooltipId),
|
@@ -101,15 +107,27 @@ export class AiCodeCompletionSummaryToolbar extends UI.Widget.Widget {
|
|
101
107
|
#loading = false;
|
102
108
|
#hasTopBorder = false;
|
103
109
|
|
110
|
+
#aidaAvailability?: Host.AidaClient.AidaAccessPreconditions;
|
111
|
+
#boundOnAidaAvailabilityChange: () => Promise<void>;
|
112
|
+
|
104
113
|
constructor(props: AiCodeCompletionSummaryToolbarProps, view?: View) {
|
105
114
|
super();
|
106
115
|
this.#disclaimerTooltipId = props.disclaimerTooltipId;
|
107
116
|
this.#citationsTooltipId = props.citationsTooltipId;
|
108
117
|
this.#hasTopBorder = props.hasTopBorder ?? false;
|
118
|
+
this.#boundOnAidaAvailabilityChange = this.#onAidaAvailabilityChange.bind(this);
|
109
119
|
this.#view = view ?? DEFAULT_SUMMARY_TOOLBAR_VIEW;
|
110
120
|
this.requestUpdate();
|
111
121
|
}
|
112
122
|
|
123
|
+
async #onAidaAvailabilityChange(): Promise<void> {
|
124
|
+
const currentAidaAvailability = await Host.AidaClient.AidaClient.checkAccessPreconditions();
|
125
|
+
if (currentAidaAvailability !== this.#aidaAvailability) {
|
126
|
+
this.#aidaAvailability = currentAidaAvailability;
|
127
|
+
this.requestUpdate();
|
128
|
+
}
|
129
|
+
}
|
130
|
+
|
113
131
|
setLoading(loading: boolean): void {
|
114
132
|
this.#loading = loading;
|
115
133
|
this.requestUpdate();
|
@@ -133,7 +151,21 @@ export class AiCodeCompletionSummaryToolbar extends UI.Widget.Widget {
|
|
133
151
|
citationsTooltipId: this.#citationsTooltipId,
|
134
152
|
loading: this.#loading,
|
135
153
|
hasTopBorder: this.#hasTopBorder,
|
154
|
+
aidaAvailability: this.#aidaAvailability,
|
136
155
|
},
|
137
156
|
undefined, this.contentElement);
|
138
157
|
}
|
158
|
+
|
159
|
+
override wasShown(): void {
|
160
|
+
super.wasShown();
|
161
|
+
Host.AidaClient.HostConfigTracker.instance().addEventListener(
|
162
|
+
Host.AidaClient.Events.AIDA_AVAILABILITY_CHANGED, this.#boundOnAidaAvailabilityChange);
|
163
|
+
void this.#onAidaAvailabilityChange();
|
164
|
+
}
|
165
|
+
|
166
|
+
override willHide(): void {
|
167
|
+
super.willHide();
|
168
|
+
Host.AidaClient.HostConfigTracker.instance().removeEventListener(
|
169
|
+
Host.AidaClient.Events.AIDA_AVAILABILITY_CHANGED, this.#boundOnAidaAvailabilityChange);
|
170
|
+
}
|
139
171
|
}
|
@@ -88,7 +88,7 @@ const lockedString = i18n.i18n.lockedString;
|
|
88
88
|
const CODE_SNIPPET_WARNING_URL = 'https://support.google.com/legal/answer/13505487';
|
89
89
|
|
90
90
|
export interface ViewInput {
|
91
|
-
aidaAvailability
|
91
|
+
aidaAvailability?: Host.AidaClient.AidaAccessPreconditions;
|
92
92
|
onAction: (event: Event) => void;
|
93
93
|
onDismiss: (event: Event) => void;
|
94
94
|
}
|
@@ -134,8 +134,9 @@ interface AiCodeCompletionTeaserConfig {
|
|
134
134
|
export class AiCodeCompletionTeaser extends UI.Widget.Widget {
|
135
135
|
readonly #view: View;
|
136
136
|
|
137
|
-
#aidaAvailability
|
137
|
+
#aidaAvailability?: Host.AidaClient.AidaAccessPreconditions;
|
138
138
|
#boundOnAidaAvailabilityChange: () => Promise<void>;
|
139
|
+
#boundOnAiCodeCompletionSettingChanged: () => void;
|
139
140
|
#onDetach: () => void;
|
140
141
|
|
141
142
|
// Whether the user completed first run experience dialog or not.
|
@@ -153,6 +154,7 @@ export class AiCodeCompletionTeaser extends UI.Widget.Widget {
|
|
153
154
|
this.#onDetach = config.onDetach;
|
154
155
|
this.#view = view ?? DEFAULT_VIEW;
|
155
156
|
this.#boundOnAidaAvailabilityChange = this.#onAidaAvailabilityChange.bind(this);
|
157
|
+
this.#boundOnAiCodeCompletionSettingChanged = this.#onAiCodeCompletionSettingChanged.bind(this);
|
156
158
|
this.#noLogging = Root.Runtime.hostConfig.aidaAvailability?.enterprisePolicyValue ===
|
157
159
|
Root.Runtime.GenAiEnterprisePolicyValue.ALLOW_WITHOUT_LOGGING;
|
158
160
|
this.requestUpdate();
|
@@ -179,6 +181,12 @@ export class AiCodeCompletionTeaser extends UI.Widget.Widget {
|
|
179
181
|
}
|
180
182
|
}
|
181
183
|
|
184
|
+
#onAiCodeCompletionSettingChanged(): void {
|
185
|
+
if (this.#aiCodeCompletionFreCompletedSetting.get() || this.#aiCodeCompletionTeaserDismissedSetting.get()) {
|
186
|
+
this.detach();
|
187
|
+
}
|
188
|
+
}
|
189
|
+
|
182
190
|
onAction = async(event: Event): Promise<void> => {
|
183
191
|
event.preventDefault();
|
184
192
|
const result = await FreDialog.show({
|
@@ -243,6 +251,8 @@ export class AiCodeCompletionTeaser extends UI.Widget.Widget {
|
|
243
251
|
super.wasShown();
|
244
252
|
Host.AidaClient.HostConfigTracker.instance().addEventListener(
|
245
253
|
Host.AidaClient.Events.AIDA_AVAILABILITY_CHANGED, this.#boundOnAidaAvailabilityChange);
|
254
|
+
this.#aiCodeCompletionFreCompletedSetting.addChangeListener(this.#boundOnAiCodeCompletionSettingChanged);
|
255
|
+
this.#aiCodeCompletionTeaserDismissedSetting.addChangeListener(this.#boundOnAiCodeCompletionSettingChanged);
|
246
256
|
void this.#onAidaAvailabilityChange();
|
247
257
|
}
|
248
258
|
|
@@ -250,6 +260,8 @@ export class AiCodeCompletionTeaser extends UI.Widget.Widget {
|
|
250
260
|
super.willHide();
|
251
261
|
Host.AidaClient.HostConfigTracker.instance().removeEventListener(
|
252
262
|
Host.AidaClient.Events.AIDA_AVAILABILITY_CHANGED, this.#boundOnAidaAvailabilityChange);
|
263
|
+
this.#aiCodeCompletionFreCompletedSetting.removeChangeListener(this.#boundOnAiCodeCompletionSettingChanged);
|
264
|
+
this.#aiCodeCompletionTeaserDismissedSetting.removeChangeListener(this.#boundOnAiCodeCompletionSettingChanged);
|
253
265
|
}
|
254
266
|
|
255
267
|
override onDetach(): void {
|