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.
Files changed (45) hide show
  1. package/front_end/Images/src/ai-explorer-badge.svg +114 -0
  2. package/front_end/Images/src/code-whisperer-badge.svg +166 -0
  3. package/front_end/Images/src/devtools-user-badge.svg +129 -0
  4. package/front_end/Images/src/dom-detective-badge.svg +136 -0
  5. package/front_end/Images/src/speedster-badge.svg +166 -0
  6. package/front_end/core/host/AidaClient.ts +2 -0
  7. package/front_end/core/host/GdpClient.ts +38 -2
  8. package/front_end/core/i18n/NumberFormatter.ts +7 -0
  9. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +3 -19
  10. package/front_end/models/ai_assistance/ai_assistance.ts +1 -1
  11. package/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.ts +7 -6
  12. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.snapshot.txt +119 -119
  13. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.ts +43 -52
  14. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.snapshot.txt +100 -100
  15. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.ts +12 -18
  16. package/front_end/models/ai_assistance/data_formatters/UnitFormatters.ts +151 -0
  17. package/front_end/models/ai_code_completion/AiCodeCompletion.ts +3 -0
  18. package/front_end/models/badges/Badge.ts +7 -4
  19. package/front_end/models/badges/DOMDetectiveBadge.ts +20 -0
  20. package/front_end/models/badges/SpeedsterBadge.ts +4 -1
  21. package/front_end/models/badges/StarterBadge.ts +5 -1
  22. package/front_end/models/badges/UserBadges.ts +33 -7
  23. package/front_end/models/trace/ModelImpl.ts +0 -13
  24. package/front_end/models/trace/insights/Common.ts +19 -0
  25. package/front_end/panels/common/AiCodeCompletionDisclaimer.ts +36 -9
  26. package/front_end/panels/common/AiCodeCompletionSummaryToolbar.ts +32 -0
  27. package/front_end/panels/common/AiCodeCompletionTeaser.ts +14 -2
  28. package/front_end/panels/common/BadgeNotification.ts +119 -9
  29. package/front_end/panels/common/badgeNotification.css +4 -0
  30. package/front_end/panels/console/ConsolePrompt.ts +26 -0
  31. package/front_end/panels/elements/ElementsTreeElement.ts +12 -0
  32. package/front_end/panels/elements/ElementsTreeOutline.ts +3 -0
  33. package/front_end/panels/elements/StylePropertiesSection.ts +3 -0
  34. package/front_end/panels/elements/StylePropertyTreeElement.ts +5 -0
  35. package/front_end/panels/settings/SettingsScreen.ts +3 -9
  36. package/front_end/panels/settings/components/SyncSection.ts +6 -2
  37. package/front_end/panels/sources/AiCodeCompletionPlugin.ts +35 -6
  38. package/front_end/panels/timeline/TimelinePanel.ts +22 -10
  39. package/front_end/panels/timeline/TimelineUIUtils.ts +4 -3
  40. package/front_end/panels/timeline/utils/InsightAIContext.ts +0 -19
  41. package/front_end/ui/legacy/filter.css +1 -1
  42. package/front_end/ui/legacy/inspectorCommon.css +1 -1
  43. package/front_end/ui/legacy/softDropDownButton.css +1 -1
  44. package/package.json +1 -1
  45. 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
- dispatchBadgeTriggeredEvent: (badge: Badge) => void;
17
+ onTriggerBadge: (badge: Badge) => void;
16
18
  badgeActionEventTarget: Common.ObjectWrapper.ObjectWrapper<BadgeActionEvents>;
17
19
  }
18
20
 
19
21
  export abstract class Badge {
20
- #dispatchBadgeTriggeredEvent: (badge: Badge) => void;
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.#dispatchBadgeTriggeredEvent = context.dispatchBadgeTriggeredEvent;
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.#dispatchBadgeTriggeredEvent(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
- override readonly name = 'awards/speedster';
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
- override readonly name = 'awards/chrome-devtools-user';
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?: Common.Settings.Setting<Boolean>;
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
- UserBadges.BADGE_REGISTRY.map(badgeCtor => new badgeCtor({
42
- dispatchBadgeTriggeredEvent: this.#dispatchBadgeTriggeredEvent.bind(this),
43
- badgeActionEventTarget: this.#badgeActionEventTarget,
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
- #dispatchBadgeTriggeredEvent(badge: Badge): void {
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
- (input, output, target) => {
62
- if (!input.disclaimerTooltipId) {
63
- render(nothing, target);
64
- return;
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
- // clang-format on
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: Host.AidaClient.AidaAccessPreconditions;
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 = Host.AidaClient.AidaAccessPreconditions.NO_ACCOUNT_EMAIL;
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 {