chrome-devtools-frontend 1.0.1528866 → 1.0.1529186

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 (38) hide show
  1. package/front_end/core/host/UserMetrics.ts +2 -1
  2. package/front_end/core/root/Runtime.ts +10 -0
  3. package/front_end/core/sdk/NetworkManager.ts +85 -35
  4. package/front_end/core/sdk/SourceMap.ts +4 -0
  5. package/front_end/core/sdk/SourceMapScopesInfo.ts +32 -2
  6. package/front_end/entrypoints/main/MainImpl.ts +23 -4
  7. package/front_end/generated/SupportedCSSProperties.js +2 -0
  8. package/front_end/models/ai_assistance/BuiltInAi.ts +1 -1
  9. package/front_end/models/ai_assistance/ConversationHandler.ts +15 -14
  10. package/front_end/models/javascript_metadata/NativeFunctions.js +8 -0
  11. package/front_end/models/persistence/NetworkPersistenceManager.ts +3 -5
  12. package/front_end/models/persistence/PersistenceImpl.ts +0 -5
  13. package/front_end/models/persistence/persistence-meta.ts +0 -31
  14. package/front_end/models/persistence/persistence.ts +0 -6
  15. package/front_end/{models/persistence → panels/common}/PersistenceUtils.ts +15 -17
  16. package/front_end/panels/common/common.ts +1 -0
  17. package/front_end/panels/console/ConsoleInsightTeaser.ts +285 -22
  18. package/front_end/panels/console/ConsoleViewMessage.ts +18 -1
  19. package/front_end/panels/console/console-meta.ts +14 -0
  20. package/front_end/panels/console/consoleInsightTeaser.css +28 -0
  21. package/front_end/panels/explain/ActionDelegate.ts +3 -0
  22. package/front_end/panels/explain/explain-meta.ts +7 -0
  23. package/front_end/panels/network/BlockedURLsPane.ts +139 -36
  24. package/front_end/panels/network/NetworkLogView.ts +1 -1
  25. package/front_end/{models/persistence → panels/settings}/EditFileSystemView.ts +2 -6
  26. package/front_end/panels/settings/WorkspaceSettingsTab.ts +2 -1
  27. package/front_end/panels/settings/settings.ts +2 -0
  28. package/front_end/{models/persistence → panels/sources}/PersistenceActions.ts +8 -12
  29. package/front_end/panels/sources/TabbedEditorContainer.ts +2 -1
  30. package/front_end/panels/sources/sources-meta.ts +15 -0
  31. package/front_end/panels/sources/sources.ts +2 -0
  32. package/front_end/panels/utils/utils.ts +2 -1
  33. package/front_end/third_party/chromium/README.chromium +1 -1
  34. package/front_end/third_party/diff/diff_match_patch.js +1 -1
  35. package/front_end/ui/legacy/ListWidget.ts +2 -2
  36. package/front_end/ui/visual_logging/KnownContextValues.ts +8 -0
  37. package/package.json +1 -1
  38. /package/front_end/{models/persistence → panels/settings}/editFileSystemView.css +0 -0
@@ -6,16 +6,11 @@
6
6
  import * as Common from '../../core/common/common.js';
7
7
  import * as i18n from '../../core/i18n/i18n.js';
8
8
  import * as Platform from '../../core/platform/platform.js';
9
+ import * as Persistence from '../../models/persistence/persistence.js';
10
+ import * as Workspace from '../../models/workspace/workspace.js';
9
11
  import * as IconButton from '../../ui/components/icon_button/icon_button.js';
10
12
  import * as Components from '../../ui/legacy/components/utils/utils.js';
11
- // TODO(crbug.com/442509324): remove UI dependency
12
- // eslint-disable-next-line rulesdir/no-imports-in-directory
13
13
  import * as UI from '../../ui/legacy/legacy.js';
14
- import * as Workspace from '../workspace/workspace.js';
15
-
16
- import {FileSystemWorkspaceBinding} from './FileSystemWorkspaceBinding.js';
17
- import {NetworkPersistenceManager} from './NetworkPersistenceManager.js';
18
- import {Events, type PersistenceBinding, PersistenceImpl} from './PersistenceImpl.js';
19
14
 
20
15
  const UIStrings = {
21
16
  /**
@@ -29,16 +24,17 @@ const UIStrings = {
29
24
  */
30
25
  linkedToS: 'Linked to {PH1}',
31
26
  } as const;
32
- const str_ = i18n.i18n.registerUIStrings('models/persistence/PersistenceUtils.ts', UIStrings);
27
+ const str_ = i18n.i18n.registerUIStrings('panels/common/PersistenceUtils.ts', UIStrings);
33
28
  const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
34
29
  export class PersistenceUtils {
35
30
  static tooltipForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): string {
36
- const binding = PersistenceImpl.instance().binding(uiSourceCode);
31
+ const binding = Persistence.Persistence.PersistenceImpl.instance().binding(uiSourceCode);
37
32
  if (!binding) {
38
33
  return '';
39
34
  }
40
35
  if (uiSourceCode === binding.network) {
41
- return FileSystemWorkspaceBinding.tooltipForUISourceCode(binding.fileSystem);
36
+ return Persistence.FileSystemWorkspaceBinding.FileSystemWorkspaceBinding.tooltipForUISourceCode(
37
+ binding.fileSystem);
42
38
  }
43
39
  if (binding.network.contentType().isFromSourceMap()) {
44
40
  return i18nString(
@@ -48,7 +44,7 @@ export class PersistenceUtils {
48
44
  }
49
45
 
50
46
  static iconForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): IconButton.Icon.Icon|null {
51
- const binding = PersistenceImpl.instance().binding(uiSourceCode);
47
+ const binding = Persistence.Persistence.PersistenceImpl.instance().binding(uiSourceCode);
52
48
  if (binding) {
53
49
  if (!Common.ParsedURL.schemeIs(binding.fileSystem.url(), 'file:')) {
54
50
  return null;
@@ -57,7 +53,8 @@ export class PersistenceUtils {
57
53
  icon.name = 'document';
58
54
  icon.classList.add('small');
59
55
  UI.Tooltip.Tooltip.install(icon, PersistenceUtils.tooltipForUISourceCode(binding.network));
60
- if (NetworkPersistenceManager.instance().project() === binding.fileSystem.project()) {
56
+ if (Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance().project() ===
57
+ binding.fileSystem.project()) {
61
58
  icon.classList.add('dot', 'purple');
62
59
  } else {
63
60
  icon.classList.add('dot', 'green');
@@ -70,7 +67,8 @@ export class PersistenceUtils {
70
67
  return null;
71
68
  }
72
69
 
73
- if (NetworkPersistenceManager.instance().isActiveHeaderOverrides(uiSourceCode)) {
70
+ if (Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance().isActiveHeaderOverrides(
71
+ uiSourceCode)) {
74
72
  const icon = new IconButton.Icon.Icon();
75
73
  icon.name = 'document';
76
74
  icon.classList.add('small');
@@ -88,13 +86,13 @@ export class PersistenceUtils {
88
86
 
89
87
  export class LinkDecorator extends Common.ObjectWrapper.ObjectWrapper<Components.Linkifier.LinkDecorator.EventTypes>
90
88
  implements Components.Linkifier.LinkDecorator {
91
- constructor(persistence: PersistenceImpl) {
89
+ constructor(persistence: Persistence.Persistence.PersistenceImpl) {
92
90
  super();
93
- persistence.addEventListener(Events.BindingCreated, this.bindingChanged, this);
94
- persistence.addEventListener(Events.BindingRemoved, this.bindingChanged, this);
91
+ persistence.addEventListener(Persistence.Persistence.Events.BindingCreated, this.bindingChanged, this);
92
+ persistence.addEventListener(Persistence.Persistence.Events.BindingRemoved, this.bindingChanged, this);
95
93
  }
96
94
 
97
- private bindingChanged(event: Common.EventTarget.EventTargetEvent<PersistenceBinding>): void {
95
+ private bindingChanged(event: Common.EventTarget.EventTargetEvent<Persistence.Persistence.PersistenceBinding>): void {
98
96
  const binding = event.data;
99
97
  this.dispatchEventToListeners(Components.Linkifier.LinkDecorator.Events.LINK_ICON_CHANGED, binding.network);
100
98
  }
@@ -100,3 +100,4 @@ export {GdpSignUpDialog} from './GdpSignUpDialog.js';
100
100
  export {AiCodeCompletionDisclaimer} from './AiCodeCompletionDisclaimer.js';
101
101
  export {AiCodeCompletionSummaryToolbar} from './AiCodeCompletionSummaryToolbar.js';
102
102
  export * from './BadgeNotification.js';
103
+ export * as PersistenceUtils from './PersistenceUtils.js';
@@ -4,16 +4,55 @@
4
4
 
5
5
  import '../../ui/components/tooltips/tooltips.js';
6
6
 
7
+ import * as Common from '../../core/common/common.js';
8
+ import * as Host from '../../core/host/host.js';
7
9
  import * as i18n from '../../core/i18n/i18n.js';
10
+ import * as Root from '../../core/root/root.js';
11
+ import * as AiAssistanceModel from '../../models/ai_assistance/ai_assistance.js';
12
+ import * as Buttons from '../../ui/components/buttons/buttons.js';
8
13
  import * as UI from '../../ui/legacy/legacy.js';
9
14
  import * as Lit from '../../ui/lit/lit.js';
15
+ import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
16
+ import * as PanelCommon from '../common/common.js';
10
17
 
11
18
  import consoleInsightTeaserStyles from './consoleInsightTeaser.css.js';
12
- import type {ConsoleViewMessage} from './ConsoleViewMessage.js';
19
+ import {ConsoleViewMessage} from './ConsoleViewMessage.js';
20
+ import {PromptBuilder} from './PromptBuilder.js';
13
21
 
14
22
  const {render, html} = Lit;
15
23
 
16
24
  const UIStringsNotTranslate = {
25
+ /**
26
+ * @description Link text in the disclaimer dialog, linking to a settings page containing more information
27
+ */
28
+ learnMore: 'Learn more about AI summaries',
29
+ /**
30
+ * @description Description of the console insights feature
31
+ */
32
+ freDisclaimerHeader: 'Get explanations for console warnings and errors',
33
+ /**
34
+ * @description First item in the first-run experience dialog
35
+ */
36
+ freDisclaimerTextAiWontAlwaysGetItRight: 'This feature uses AI and won’t always get it right',
37
+ /**
38
+ * @description Explainer for which data is being sent by the console insights feature
39
+ */
40
+ consoleInsightsSendsData:
41
+ 'To generate explanations, the console message, associated stack trace, related source code, and the associated network headers are sent to Google. This data may be seen by human reviewers to improve this feature.',
42
+ /**
43
+ * @description Explainer for which data is being sent by the console insights feature
44
+ */
45
+ consoleInsightsSendsDataNoLogging:
46
+ 'To generate explanations, the console message, associated stack trace, related source code, and the associated network headers are sent to Google. This data will not be used to improve Google’s AI models. Your organization may change these settings at any time.',
47
+ /**
48
+ * @description Third item in the first-run experience dialog
49
+ */
50
+ freDisclaimerTextUseWithCaution: 'Use generated code snippets with caution',
51
+ /**
52
+ * @description Tooltip text for the console insights teaser
53
+ */
54
+ infoTooltipText:
55
+ 'The text above has been generated with AI on your local device. Clicking the button will send the console message, stack trace, related source code, and the associated network headers to Google to generate a more detailed explanation.',
17
56
  /**
18
57
  * @description Header text during loading state while an AI summary is being generated
19
58
  */
@@ -22,15 +61,32 @@ const UIStringsNotTranslate = {
22
61
  * @description Label for an animation shown while an AI response is being generated
23
62
  */
24
63
  loading: 'Loading',
64
+ /**
65
+ * @description Label for a button which generates a more detailed explanation
66
+ */
67
+ tellMeMore: 'Tell me more',
68
+ /**
69
+ * @description Label for a checkbox which turns off the teaser explanation feature
70
+ */
71
+ dontShow: 'Don’t show',
25
72
  } as const;
26
73
 
27
74
  const lockedString = i18n.i18n.lockedString;
28
75
 
76
+ const CODE_SNIPPET_WARNING_URL = 'https://support.google.com/legal/answer/13505487';
77
+ const DATA_USAGE_URL = 'https://developer.chrome.com/docs/devtools/ai-assistance/get-started#data-use';
78
+ const EXPLAIN_TEASER_ACTION_ID = 'explain.console-message.teaser';
79
+
29
80
  interface ViewInput {
81
+ onTellMeMoreClick: (event: Event) => void;
30
82
  // If multiple ConsoleInsightTeasers exist, each one needs a unique id. Otherwise showing and
31
83
  // hiding of the tooltip, and rendering the loading animation, does not work correctly.
32
- uuid: String;
84
+ uuid: string;
85
+ headerText: string;
86
+ mainText: string;
33
87
  isInactive: boolean;
88
+ dontShowChanged: (e: Event) => void;
89
+ hasTellMeMoreButton: boolean;
34
90
  }
35
91
 
36
92
  export const DEFAULT_VIEW = (input: ViewInput, _output: undefined, target: HTMLElement): void => {
@@ -39,6 +95,7 @@ export const DEFAULT_VIEW = (input: ViewInput, _output: undefined, target: HTMLE
39
95
  return;
40
96
  }
41
97
 
98
+ const showPlaceholder = !Boolean(input.mainText);
42
99
  // clang-format off
43
100
  render(html`
44
101
  <style>${consoleInsightTeaserStyles}</style>
@@ -50,23 +107,62 @@ export const DEFAULT_VIEW = (input: ViewInput, _output: undefined, target: HTMLE
50
107
  prefer-span-left
51
108
  >
52
109
  <div class="teaser-tooltip-container">
53
- <h2 tabindex="-1">${lockedString(UIStringsNotTranslate.summarizing)}</h2>
54
- <div
55
- role="presentation"
56
- aria-label=${lockedString(UIStringsNotTranslate.loading)}
57
- class="loader"
58
- style="clip-path: url(${'#clipPath-' + input.uuid});"
59
- >
60
- <svg width="100%" height="52">
61
- <defs>
62
- <clipPath id=${'clipPath-' + input.uuid}>
63
- <rect x="0" y="0" width="100%" height="12" rx="8"></rect>
64
- <rect x="0" y="20" width="100%" height="12" rx="8"></rect>
65
- <rect x="0" y="40" width="100%" height="12" rx="8"></rect>
66
- </clipPath>
67
- </defs>
68
- </svg>
69
- </div>
110
+ ${showPlaceholder ? html`
111
+ <h2 tabindex="-1">${lockedString(UIStringsNotTranslate.summarizing)}</h2>
112
+ <div
113
+ role="presentation"
114
+ aria-label=${lockedString(UIStringsNotTranslate.loading)}
115
+ class="loader"
116
+ style="clip-path: url(${'#clipPath-' + input.uuid});"
117
+ >
118
+ <svg width="100%" height="52">
119
+ <defs>
120
+ <clipPath id=${'clipPath-' + input.uuid}>
121
+ <rect x="0" y="0" width="100%" height="12" rx="8"></rect>
122
+ <rect x="0" y="20" width="100%" height="12" rx="8"></rect>
123
+ <rect x="0" y="40" width="100%" height="12" rx="8"></rect>
124
+ </clipPath>
125
+ </defs>
126
+ </svg>
127
+ </div>
128
+ ` : html`
129
+ <h2 tabindex="-1">${input.headerText}</h2>
130
+ <div>${input.mainText}</div>
131
+ <div class="tooltip-footer">
132
+ ${input.hasTellMeMoreButton ? html`
133
+ <devtools-button
134
+ title=${lockedString(UIStringsNotTranslate.tellMeMore)}
135
+ .jslogContext=${'insights-teaser-tell-me-more'},
136
+ .variant=${Buttons.Button.Variant.PRIMARY}
137
+ @click=${input.onTellMeMoreClick}
138
+ >
139
+ <devtools-icon class="lightbulb-icon" name="lightbulb-spark"></devtools-icon>
140
+ ${lockedString(UIStringsNotTranslate.tellMeMore)}
141
+ </devtools-button>
142
+ ` : Lit.nothing}
143
+ <devtools-icon
144
+ name="info"
145
+ class="info-icon"
146
+ aria-details=${'teaser-info-tooltip-' + input.uuid}
147
+ ></devtools-icon>
148
+ <devtools-tooltip id=${'teaser-info-tooltip-' + input.uuid} variant="rich">
149
+ <div class="info-tooltip-text">${lockedString(UIStringsNotTranslate.infoTooltipText)}</div>
150
+ <div class="learn-more">
151
+ <x-link
152
+ class="devtools-link"
153
+ title=${lockedString(UIStringsNotTranslate.learnMore)}
154
+ href=${DATA_USAGE_URL}
155
+ jslog=${VisualLogging.link().track({click: true, keydown:'Enter|Space'}).context('explain.teaser.learn-more')}
156
+ >${lockedString(UIStringsNotTranslate.learnMore)}</x-link>
157
+ </div>
158
+ </devtools-tooltip>
159
+ <devtools-checkbox
160
+ @change=${input.dontShowChanged}
161
+ jslog=${VisualLogging.toggle('explain.teaser.dont-show').track({ change: true })}>
162
+ ${lockedString(UIStringsNotTranslate.dontShow)}
163
+ </devtools-checkbox>
164
+ </div>
165
+ `}
70
166
  </div>
71
167
  </devtools-tooltip>
72
168
  `, target);
@@ -77,16 +173,110 @@ export type View = typeof DEFAULT_VIEW;
77
173
 
78
174
  export class ConsoleInsightTeaser extends UI.Widget.Widget {
79
175
  #view: View;
80
- #uuid: String;
176
+ #uuid: string;
177
+ #isGenerating = false;
178
+ #builtInAi: AiAssistanceModel.BuiltInAi.BuiltInAi|undefined;
179
+ #promptBuilder: PromptBuilder;
180
+ #headerText = '';
181
+ #mainText = '';
182
+ #consoleViewMessage: ConsoleViewMessage;
81
183
  #isInactive = false;
184
+ #abortController: null|AbortController = null;
82
185
 
83
- constructor(uuid: String, consoleViewMessage: ConsoleViewMessage, element?: HTMLElement, view?: View) {
186
+ constructor(uuid: string, consoleViewMessage: ConsoleViewMessage, element?: HTMLElement, view?: View) {
84
187
  super(element);
85
188
  this.#view = view ?? DEFAULT_VIEW;
86
189
  this.#uuid = uuid;
190
+ this.#promptBuilder = new PromptBuilder(consoleViewMessage);
191
+ this.#consoleViewMessage = consoleViewMessage;
87
192
  this.requestUpdate();
88
193
  }
89
194
 
195
+ #getConsoleInsightsEnabledSetting(): Common.Settings.Setting<boolean>|undefined {
196
+ try {
197
+ return Common.Settings.moduleSetting('console-insights-enabled') as Common.Settings.Setting<boolean>;
198
+ } catch {
199
+ return;
200
+ }
201
+ }
202
+
203
+ #getOnboardingCompletedSetting(): Common.Settings.Setting<boolean> {
204
+ return Common.Settings.Settings.instance().createLocalSetting('console-insights-onboarding-finished', true);
205
+ }
206
+
207
+ #executeConsoleInsightAction(): void {
208
+ UI.Context.Context.instance().setFlavor(ConsoleViewMessage, this.#consoleViewMessage);
209
+ const action = UI.ActionRegistry.ActionRegistry.instance().getAction(EXPLAIN_TEASER_ACTION_ID);
210
+ void action.execute();
211
+ }
212
+
213
+ #onTellMeMoreClick(event: Event): void {
214
+ event.stopPropagation();
215
+ if (this.#getConsoleInsightsEnabledSetting()?.getIfNotDisabled() &&
216
+ this.#getOnboardingCompletedSetting()?.getIfNotDisabled()) {
217
+ this.#executeConsoleInsightAction();
218
+ return;
219
+ }
220
+ void this.#showFreDialog();
221
+ }
222
+
223
+ async #showFreDialog(): Promise<void> {
224
+ const noLogging = Root.Runtime.hostConfig.aidaAvailability?.enterprisePolicyValue ===
225
+ Root.Runtime.GenAiEnterprisePolicyValue.ALLOW_WITHOUT_LOGGING;
226
+ const result = await PanelCommon.FreDialog.show({
227
+ header: {iconName: 'smart-assistant', text: lockedString(UIStringsNotTranslate.freDisclaimerHeader)},
228
+ reminderItems: [
229
+ {
230
+ iconName: 'psychiatry',
231
+ content: lockedString(UIStringsNotTranslate.freDisclaimerTextAiWontAlwaysGetItRight),
232
+ },
233
+ {
234
+ iconName: 'google',
235
+ content: noLogging ? lockedString(UIStringsNotTranslate.consoleInsightsSendsDataNoLogging) :
236
+ lockedString(UIStringsNotTranslate.consoleInsightsSendsData),
237
+ },
238
+ {
239
+ iconName: 'warning',
240
+ // clang-format off
241
+ content: html`<x-link
242
+ href=${CODE_SNIPPET_WARNING_URL}
243
+ class="link devtools-link"
244
+ jslog=${VisualLogging.link('explain.teaser.code-snippets-explainer').track({
245
+ click: true
246
+ })}
247
+ >${lockedString(UIStringsNotTranslate.freDisclaimerTextUseWithCaution)}</x-link>`,
248
+ // clang-format on
249
+ }
250
+ ],
251
+ onLearnMoreClick: () => {
252
+ void UI.ViewManager.ViewManager.instance().showView('chrome-ai');
253
+ },
254
+ ariaLabel: lockedString(UIStringsNotTranslate.freDisclaimerHeader),
255
+ learnMoreButtonText: lockedString(UIStringsNotTranslate.learnMore),
256
+ });
257
+
258
+ if (result) {
259
+ this.#getConsoleInsightsEnabledSetting()?.set(true);
260
+ this.#getOnboardingCompletedSetting()?.set(true);
261
+ this.#executeConsoleInsightAction();
262
+ }
263
+ }
264
+
265
+ maybeGenerateTeaser(): void {
266
+ this.requestUpdate();
267
+ if (!this.#isInactive && !this.#isGenerating && !Boolean(this.#mainText) &&
268
+ Common.Settings.Settings.instance().moduleSetting('console-insight-teasers-enabled').get()) {
269
+ void this.#generateTeaserText();
270
+ }
271
+ }
272
+
273
+ abortTeaserGeneration(): void {
274
+ if (this.#abortController) {
275
+ this.#abortController.abort();
276
+ }
277
+ this.#isGenerating = false;
278
+ }
279
+
90
280
  setInactive(isInactive: boolean): void {
91
281
  if (this.#isInactive === isInactive) {
92
282
  return;
@@ -95,11 +285,84 @@ export class ConsoleInsightTeaser extends UI.Widget.Widget {
95
285
  this.requestUpdate();
96
286
  }
97
287
 
288
+ async #generateTeaserText(): Promise<void> {
289
+ this.#isGenerating = true;
290
+ let teaserText = '';
291
+ try {
292
+ for await (const chunk of this.#getOnDeviceInsight()) {
293
+ teaserText += chunk;
294
+ }
295
+ } catch (err) {
296
+ // Ignore `AbortError` errors, which are thrown on mouse leave.
297
+ if (err.name !== 'AbortError') {
298
+ console.error(err.name, err.message);
299
+ }
300
+ this.#isGenerating = false;
301
+ return;
302
+ }
303
+
304
+ // TODO(crbug.com/443618746): Add user-facing error message instead of staying in loading state
305
+ let responseObject = {
306
+ header: null,
307
+ explanation: null,
308
+ };
309
+ try {
310
+ responseObject = JSON.parse(teaserText);
311
+ } catch (err) {
312
+ console.error(err.name, err.message);
313
+ }
314
+ this.#headerText = responseObject.header || '';
315
+ this.#mainText = responseObject.explanation || '';
316
+ this.#isGenerating = false;
317
+ this.requestUpdate();
318
+ }
319
+
320
+ async * #getOnDeviceInsight(): AsyncGenerator<string> {
321
+ const {prompt} = await this.#promptBuilder.buildPrompt();
322
+ if (!this.#builtInAi) {
323
+ this.#builtInAi = await AiAssistanceModel.BuiltInAi.BuiltInAi.instance();
324
+ if (!this.#builtInAi) {
325
+ this.#isInactive = true;
326
+ throw new Error('Cannot instantiate BuiltInAi');
327
+ }
328
+ }
329
+ this.#abortController = new AbortController();
330
+ const stream = this.#builtInAi.getConsoleInsight(prompt, this.#abortController);
331
+ for await (const chunk of stream) {
332
+ yield chunk;
333
+ }
334
+ this.#abortController = null;
335
+ }
336
+
337
+ #dontShowChanged(e: Event): void {
338
+ const showTeasers = !(e.target as HTMLInputElement).checked;
339
+ Common.Settings.Settings.instance().moduleSetting('console-insight-teasers-enabled').set(showTeasers);
340
+ }
341
+
342
+ #hasTellMeMoreButton(): boolean {
343
+ if (!UI.ActionRegistry.ActionRegistry.instance().hasAction(EXPLAIN_TEASER_ACTION_ID)) {
344
+ return false;
345
+ }
346
+ if (Root.Runtime.hostConfig.aidaAvailability?.blockedByAge || Root.Runtime.hostConfig.isOffTheRecord) {
347
+ return false;
348
+ }
349
+ if (!Host.AidaClient.AidaAccessPreconditions.AVAILABLE) {
350
+ return false;
351
+ }
352
+ return true;
353
+ }
354
+
98
355
  override performUpdate(): Promise<void>|void {
99
356
  this.#view(
100
357
  {
358
+ onTellMeMoreClick: this.#onTellMeMoreClick.bind(this),
101
359
  uuid: this.#uuid,
102
- isInactive: this.#isInactive,
360
+ headerText: this.#headerText,
361
+ mainText: this.#mainText,
362
+ isInactive: this.#isInactive ||
363
+ !Common.Settings.Settings.instance().moduleSetting('console-insight-teasers-enabled').get(),
364
+ dontShowChanged: this.#dontShowChanged.bind(this),
365
+ hasTellMeMoreButton: this.#hasTellMeMoreButton(),
103
366
  },
104
367
  undefined, this.contentElement);
105
368
  }
@@ -1343,6 +1343,17 @@ export class ConsoleViewMessage implements ConsoleViewportElement {
1343
1343
  return this.contentElementInternal;
1344
1344
  }
1345
1345
 
1346
+ #onMouseEnter(_event: MouseEvent): void {
1347
+ if (this.#teaser &&
1348
+ Common.Settings.Settings.instance().moduleSetting('console-insight-teasers-enabled').getIfNotDisabled()) {
1349
+ this.#teaser.maybeGenerateTeaser();
1350
+ }
1351
+ }
1352
+
1353
+ #onMouseLeave(): void {
1354
+ this.#teaser?.abortTeaserGeneration();
1355
+ }
1356
+
1346
1357
  toMessageElement(): HTMLElement {
1347
1358
  if (this.elementInternal) {
1348
1359
  return this.elementInternal;
@@ -1350,6 +1361,8 @@ export class ConsoleViewMessage implements ConsoleViewportElement {
1350
1361
  this.elementInternal = document.createElement('div');
1351
1362
  this.elementInternal.tabIndex = -1;
1352
1363
  this.elementInternal.addEventListener('keydown', (this.onKeyDown.bind(this) as EventListener));
1364
+ this.elementInternal.addEventListener('mouseenter', this.#onMouseEnter.bind(this));
1365
+ this.elementInternal.addEventListener('mouseleave', this.#onMouseLeave.bind(this));
1353
1366
  this.updateMessageElement();
1354
1367
  this.elementInternal.classList.toggle('console-adjacent-user-command-result', this.#adjacentUserCommandResult);
1355
1368
  return this.elementInternal;
@@ -1447,7 +1460,11 @@ export class ConsoleViewMessage implements ConsoleViewportElement {
1447
1460
  }
1448
1461
 
1449
1462
  shouldShowTeaser(): boolean {
1450
- if (!this.shouldShowInsights() || !AiAssistanceModel.BuiltInAi.BuiltInAi.cachedIsAvailable()) {
1463
+ if (!this.shouldShowInsights()) {
1464
+ return false;
1465
+ }
1466
+ if (!Common.Settings.Settings.instance().moduleSetting('console-insight-teasers-enabled').getIfNotDisabled() ||
1467
+ !AiAssistanceModel.BuiltInAi.BuiltInAi.cachedIsAvailable()) {
1451
1468
  return false;
1452
1469
  }
1453
1470
  const devtoolsLocale = i18n.DevToolsLocale.DevToolsLocale.instance();
@@ -133,6 +133,11 @@ const UIStrings = {
133
133
  * @description Title of a setting under the Console category in Settings that controls whether `console.trace()` messages appear collapsed by default.
134
134
  */
135
135
  collapseConsoleTraceMessagesByDefault: 'Do not automatically expand `console.trace()` messages',
136
+ /**
137
+ * @description Title of a setting under the Console category in Settings that controls whether AI summaries should
138
+ * be shown for console warnings/errors.
139
+ */
140
+ showConsoleInsightTeasers: 'Show AI summaries for console messages',
136
141
  } as const;
137
142
  const str_ = i18n.i18n.registerUIStrings('panels/console/console-meta.ts', UIStrings);
138
143
  const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_);
@@ -437,6 +442,15 @@ Common.Settings.registerSettingExtension({
437
442
  ],
438
443
  });
439
444
 
445
+ Common.Settings.registerSettingExtension({
446
+ category: Common.Settings.SettingCategory.CONSOLE,
447
+ storageType: Common.Settings.SettingStorageType.SYNCED,
448
+ title: i18nLazyString(UIStrings.showConsoleInsightTeasers),
449
+ settingName: 'console-insight-teasers-enabled',
450
+ settingType: Common.Settings.SettingType.BOOLEAN,
451
+ defaultValue: true,
452
+ });
453
+
440
454
  Common.Revealer.registerRevealer({
441
455
  contextTypes() {
442
456
  return [
@@ -52,4 +52,32 @@
52
52
  font: var(--sys-typescale-body4-bold);
53
53
  margin: 0 0 var(--sys-size-3);
54
54
  }
55
+
56
+ .lightbulb-icon {
57
+ color: var(--sys-color-on-primary);
58
+ height: var(--sys-size-7);
59
+ margin-left: calc(-1 * var(--sys-size-4));
60
+ }
61
+
62
+ .learn-more {
63
+ padding-top: 7px;
64
+ }
65
+
66
+ .info-tooltip-text {
67
+ max-width: var(--sys-size-26);
68
+ }
69
+
70
+ .tooltip-footer {
71
+ padding: var(--sys-size-5) 0 0;
72
+ display: flex;
73
+ align-items: center;
74
+
75
+ .info-icon {
76
+ margin-left: var(--sys-size-4);
77
+ }
78
+
79
+ devtools-checkbox {
80
+ margin-left: auto;
81
+ }
82
+ }
55
83
  }
@@ -15,11 +15,14 @@ export class ActionDelegate implements UI.ActionRegistration.ActionDelegate {
15
15
  case 'explain.console-message.context.error':
16
16
  case 'explain.console-message.context.warning':
17
17
  case 'explain.console-message.context.other':
18
+ case 'explain.console-message.teaser':
18
19
  case 'explain.console-message.hover': {
19
20
  const consoleViewMessage = context.flavor(Console.ConsoleViewMessage.ConsoleViewMessage);
20
21
  if (consoleViewMessage) {
21
22
  if (actionId.startsWith('explain.console-message.context')) {
22
23
  Host.userMetrics.actionTaken(Host.UserMetrics.Action.InsightRequestedViaContextMenu);
24
+ } else if (actionId === 'explain.console-message.teaser') {
25
+ Host.userMetrics.actionTaken(Host.UserMetrics.Action.InsightRequestedViaTeaser);
23
26
  } else if (actionId === 'explain.console-message.hover') {
24
27
  Host.userMetrics.actionTaken(Host.UserMetrics.Action.InsightRequestedViaHoverButton);
25
28
  }
@@ -57,6 +57,13 @@ const actions = [
57
57
  return [Console.ConsoleViewMessage.ConsoleViewMessage];
58
58
  },
59
59
  },
60
+ {
61
+ actionId: 'explain.console-message.teaser',
62
+ title: i18nLazyString(UIStrings.explainThisMessage),
63
+ contextTypes(): [] {
64
+ return [];
65
+ },
66
+ },
60
67
  {
61
68
  actionId: 'explain.console-message.context.error',
62
69
  title: i18nLazyString(UIStrings.explainThisError),