chrome-devtools-frontend 1.0.1646286 → 1.0.1649421

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 (57) hide show
  1. package/SECURITY.md +1 -1
  2. package/extension-api/ExtensionAPI.d.ts +26 -0
  3. package/front_end/core/common/Debouncer.ts +9 -1
  4. package/front_end/core/sdk/CSSMatchedStyles.ts +3 -1
  5. package/front_end/core/sdk/CSSMetadata.ts +1 -0
  6. package/front_end/core/sdk/CSSPropertyParserMatchers.ts +65 -0
  7. package/front_end/generated/InspectorBackendCommands.ts +2 -1
  8. package/front_end/generated/SupportedCSSProperties.js +739 -21
  9. package/front_end/generated/protocol.ts +26 -0
  10. package/front_end/models/ai_assistance/AiAgent2.ts +14 -9
  11. package/front_end/models/ai_assistance/agents/AiAgent.ts +10 -3
  12. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +7 -0
  13. package/front_end/models/ai_assistance/agents/StorageAgent.ts +45 -0
  14. package/front_end/models/ai_assistance/ai_assistance.ts +4 -0
  15. package/front_end/models/ai_assistance/skills/Skill.ts +1 -1
  16. package/front_end/models/ai_assistance/skills/SkillRegistry.ts +2 -0
  17. package/front_end/models/ai_assistance/skills/network.md +16 -0
  18. package/front_end/models/ai_assistance/skills/styling.md +1 -1
  19. package/front_end/models/ai_assistance/tools/GetNetworkRequestDetails.ts +118 -0
  20. package/front_end/models/ai_assistance/tools/ListNetworkRequests.ts +124 -0
  21. package/front_end/models/ai_assistance/tools/Tool.ts +2 -0
  22. package/front_end/models/ai_assistance/tools/ToolRegistry.ts +4 -0
  23. package/front_end/models/emulation/EmulatedDevices.ts +430 -12
  24. package/front_end/models/extensions/ExtensionAPI.ts +47 -21
  25. package/front_end/models/javascript_metadata/NativeFunctions.js +5 -1
  26. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +40 -1
  27. package/front_end/panels/ai_assistance/ai_assistance.ts +1 -0
  28. package/front_end/panels/ai_assistance/components/AIv2MarkdownRenderer.ts +228 -0
  29. package/front_end/panels/ai_assistance/components/ChatMessage.ts +39 -2
  30. package/front_end/panels/application/ApplicationPanelSidebar.ts +4 -0
  31. package/front_end/panels/application/CookieItemsView.ts +55 -6
  32. package/front_end/panels/application/DOMStorageItemsView.ts +43 -8
  33. package/front_end/panels/application/KeyValueStorageItemsView.ts +17 -9
  34. package/front_end/panels/application/WebMCPTreeElement.ts +8 -0
  35. package/front_end/panels/elements/StylePropertyTreeElement.ts +73 -16
  36. package/front_end/panels/elements/StylesAiCodeCompletionProvider.ts +9 -1
  37. package/front_end/panels/elements/StylesSidebarPane.ts +22 -8
  38. package/front_end/panels/layer_viewer/PaintProfilerView.ts +309 -182
  39. package/front_end/panels/layer_viewer/paintProfiler.css +27 -23
  40. package/front_end/panels/layers/LayerPaintProfilerView.ts +86 -29
  41. package/front_end/panels/sources/WatchExpressionsSidebarPane.ts +210 -356
  42. package/front_end/panels/sources/watchExpressionsSidebarPane.css +7 -0
  43. package/front_end/panels/timeline/TimelinePaintProfilerView.ts +5 -5
  44. package/front_end/panels/timeline/components/NetworkTrackWidget.ts +122 -0
  45. package/front_end/panels/timeline/components/components.ts +2 -0
  46. package/front_end/panels/timeline/components/networkTrackWidget.css +31 -0
  47. package/front_end/panels/timeline/timeline.ts +2 -0
  48. package/front_end/third_party/chromium/README.chromium +1 -1
  49. package/front_end/ui/components/buttons/FloatingButton.ts +17 -2
  50. package/front_end/ui/components/buttons/floatingButton.css +2 -2
  51. package/front_end/ui/components/text_editor/AiCodeCompletionProvider.ts +1 -1
  52. package/front_end/ui/legacy/TextPrompt.ts +24 -18
  53. package/front_end/ui/legacy/Treeoutline.ts +31 -4
  54. package/front_end/ui/legacy/components/cookie_table/CookiesTable.ts +50 -24
  55. package/front_end/ui/legacy/components/utils/Linkifier.ts +10 -0
  56. package/front_end/ui/visual_logging/KnownContextValues.ts +13 -0
  57. package/package.json +1 -1
@@ -18,6 +18,7 @@ import type * as LHModel from '../../models/lighthouse/lighthouse.js';
18
18
  import type * as Trace from '../../models/trace/trace.js';
19
19
  import * as Workspace from '../../models/workspace/workspace.js';
20
20
  import * as Buttons from '../../ui/components/buttons/buttons.js';
21
+ import type * as MarkdownView from '../../ui/components/markdown_view/markdown_view.js';
21
22
  import * as Snackbars from '../../ui/components/snackbars/snackbars.js';
22
23
  import * as UIHelpers from '../../ui/helpers/helpers.js';
23
24
  import * as UI from '../../ui/legacy/legacy.js';
@@ -30,6 +31,10 @@ import * as TimelinePanel from '../timeline/timeline.js';
30
31
 
31
32
  import aiAssistancePanelStyles from './aiAssistancePanel.css.js';
32
33
  import {AccessibilityAgentMarkdownRenderer} from './components/AccessibilityAgentMarkdownRenderer.js';
34
+ import {
35
+ AIv2MarkdownRenderer,
36
+ type AIv2MarkdownRendererOptions,
37
+ } from './components/AIv2MarkdownRenderer.js';
33
38
  import {
34
39
  type AnswerPart,
35
40
  ChatMessageEntity,
@@ -349,6 +354,7 @@ async function getEmptyStateSuggestions(conversation?: AiAssistanceModel.AiConve
349
354
  return [
350
355
  {title: 'How is localStorage used on this page?', jslogContext: 'storage-default'},
351
356
  {title: 'How is sessionStorage used on this page?', jslogContext: 'storage-default'},
357
+ {title: 'What cookies are stored for this page?', jslogContext: 'storage-default'},
352
358
  ];
353
359
  }
354
360
 
@@ -357,8 +363,41 @@ async function getEmptyStateSuggestions(conversation?: AiAssistanceModel.AiConve
357
363
  }
358
364
  }
359
365
 
366
+ function createV2MarkdownRenderer(conversation?: AiAssistanceModel.AiConversation.AiConversation):
367
+ AIv2MarkdownRenderer {
368
+ const options: AIv2MarkdownRendererOptions = {};
369
+ const primaryTarget = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
370
+ const domModel = primaryTarget?.model(SDK.DOMModel.DOMModel);
371
+ const resourceTreeModel = primaryTarget?.model(SDK.ResourceTreeModel.ResourceTreeModel);
372
+ const context = conversation?.selectedContext;
373
+
374
+ if (context instanceof AiAssistanceModel.PerformanceAgent.PerformanceTraceContext) {
375
+ const focus = context.getItem();
376
+ options.mainFrameId = focus.parsedTrace.data.Meta.mainFrameId;
377
+ options.lookupTraceEvent = focus.lookupEvent.bind(focus);
378
+ } else {
379
+ if (domModel) {
380
+ options.mainDocumentURL = domModel.existingDocument()?.documentURL;
381
+ }
382
+ if (resourceTreeModel) {
383
+ options.mainFrameId = resourceTreeModel.mainFrame?.id;
384
+ }
385
+ }
386
+ return new AIv2MarkdownRenderer(options);
387
+ }
388
+
360
389
  function getMarkdownRenderer(conversation?: AiAssistanceModel.AiConversation.AiConversation):
361
- MarkdownRendererWithCodeBlock {
390
+ MarkdownView.MarkdownView.MarkdownInsightRenderer {
391
+ if (conversation?.type === AiAssistanceModel.AiHistoryStorage.ConversationType.PERFORMANCE &&
392
+ conversation.isReadOnly) {
393
+ // Handle historical conversations (can't linkify anything).
394
+ return new PerformanceAgentMarkdownRenderer();
395
+ }
396
+
397
+ if (Root.Runtime.hostConfig.devToolsAiV2Architecture?.enabled && conversation && !conversation.isReadOnly) {
398
+ return createV2MarkdownRenderer(conversation);
399
+ }
400
+
362
401
  const context = conversation?.selectedContext;
363
402
 
364
403
  if (context instanceof AiAssistanceModel.PerformanceAgent.PerformanceTraceContext) {
@@ -5,6 +5,7 @@
5
5
  export * from './AiAssistancePanel.js';
6
6
  export * from './components/ChatView.js';
7
7
  export * from './components/AccessibilityAgentMarkdownRenderer.js';
8
+ export * from './components/AIv2MarkdownRenderer.js';
8
9
  export * as ChatInput from './components/ChatInput.js';
9
10
  export * from './components/MarkdownRendererWithCodeBlock.js';
10
11
  export * from './SelectWorkspaceDialog.js';
@@ -0,0 +1,228 @@
1
+ // Copyright 2026 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 Common from '../../../core/common/common.js';
6
+ import * as Platform from '../../../core/platform/platform.js';
7
+ import * as SDK from '../../../core/sdk/sdk.js';
8
+ import type * as Protocol from '../../../generated/protocol.js';
9
+ import * as AiAssistanceModel from '../../../models/ai_assistance/ai_assistance.js';
10
+ import * as Logs from '../../../models/logs/logs.js';
11
+ import * as Trace from '../../../models/trace/trace.js';
12
+ import type * as Marked from '../../../third_party/marked/marked.js';
13
+ import * as MarkdownView from '../../../ui/components/markdown_view/markdown_view.js';
14
+ import * as Lit from '../../../ui/lit/lit.js';
15
+ import * as PanelsCommon from '../../common/common.js';
16
+
17
+ const {html} = Lit.StaticHtml;
18
+ const {until} = Lit.Directives;
19
+
20
+ export interface AIv2MarkdownRendererOptions {
21
+ mainFrameId?: string;
22
+ mainDocumentURL?: Platform.DevToolsPath.UrlString;
23
+ lookupTraceEvent?: (key: string) => Trace.Types.Events.Event | null;
24
+ }
25
+
26
+ type ParsedLink = {
27
+ type: 'path',
28
+ path: string,
29
+ }|{
30
+ type: 'node',
31
+ nodeId: Protocol.DOM.BackendNodeId,
32
+ };
33
+
34
+ /**
35
+ * AIv2MarkdownRenderer is currently duplicated from the agent-specific renderers
36
+ * as part of the migration to the V2 architecture. It will eventually become
37
+ * the only markdown renderer used by AI assistance.
38
+ */
39
+ export class AIv2MarkdownRenderer extends MarkdownView.MarkdownView.MarkdownInsightRenderer {
40
+ constructor(private readonly options: AIv2MarkdownRendererOptions = {}) {
41
+ super();
42
+ }
43
+
44
+ #isSameOrigin(node: SDK.DOMModel.DOMNode): boolean {
45
+ if (!this.options.mainDocumentURL) {
46
+ return true;
47
+ }
48
+ const nodeDocumentURL = node.ownerDocument?.documentURL ?? '' as Platform.DevToolsPath.UrlString;
49
+ return AiAssistanceModel.AiUtils.isSameOrigin(this.options.mainDocumentURL, nodeDocumentURL);
50
+ }
51
+
52
+ #revealableLink(revealable: unknown, label: string): Lit.LitTemplate {
53
+ return html`<devtools-link @click=${(e: Event) => {
54
+ e.preventDefault();
55
+ e.stopPropagation();
56
+ void Common.Revealer.reveal(revealable);
57
+ }}>${Platform.StringUtilities.trimEndWithMaxLength(label, 100)}</devtools-link>`;
58
+ }
59
+
60
+ #renderLink(href: string, text: string): Lit.LitTemplate|null {
61
+ const devtoolsLink = this.#renderDevToolsLink(href, text);
62
+ if (devtoolsLink) {
63
+ return devtoolsLink;
64
+ }
65
+
66
+ if (href.startsWith('#')) {
67
+ const parsed = this.#parseLink(href);
68
+ if (parsed) {
69
+ const resultPromise =
70
+ parsed.type === 'path' ? this.#linkifyPath(parsed.path, text) : this.#linkifyNode(parsed.nodeId, text);
71
+
72
+ return html`<span>${until(resultPromise.then(node => node || text), text)}</span>`;
73
+ }
74
+
75
+ if (this.options.lookupTraceEvent) {
76
+ const event = this.options.lookupTraceEvent(href.slice(1));
77
+ if (event) {
78
+ let label = text;
79
+ let title = '';
80
+ if (Trace.Types.Events.isSyntheticNetworkRequest(event)) {
81
+ title = event.args.data.url;
82
+ } else {
83
+ label += ` (${event.name})`;
84
+ }
85
+
86
+ // eslint-disable-next-line @devtools/no-a-tags-in-lit
87
+ return html`<a href="#" draggable=false .title=${title} @click=${(e: Event) => {
88
+ e.stopPropagation();
89
+ void Common.Revealer.reveal(new SDK.TraceObject.RevealableEvent(event));
90
+ }}>${label}</a>`;
91
+ }
92
+ }
93
+ }
94
+
95
+ return null;
96
+ }
97
+
98
+ #renderDevToolsLink(
99
+ href: string,
100
+ fallbackText: string,
101
+ ): Lit.LitTemplate|null {
102
+ if (href.startsWith('#req-')) {
103
+ const request = Logs.NetworkLog.NetworkLog.instance().requests().find(
104
+ req => req.requestId() === href.substring(5),
105
+ );
106
+
107
+ if (request) {
108
+ return this.#revealableLink(request, request.url());
109
+ }
110
+ return html`${fallbackText}`;
111
+ }
112
+ if (href.startsWith('#file-')) {
113
+ const file = AiAssistanceModel.ContextSelectionAgent.ContextSelectionAgent.getUISourceCodes().find(
114
+ file => AiAssistanceModel.ContextSelectionAgent.ContextSelectionAgent.uiSourceCodeId.get(file) ===
115
+ Number(href.substring(6)));
116
+
117
+ if (file) {
118
+ return this.#revealableLink(file, file.name());
119
+ }
120
+ return html`${fallbackText}`;
121
+ }
122
+ return null;
123
+ }
124
+
125
+ #parseLink(href: string): ParsedLink|null {
126
+ if (href.startsWith('#path-')) {
127
+ return {type: 'path', path: href.replace('#path-', '')};
128
+ }
129
+ if (href.startsWith('#1,HTML')) {
130
+ return {type: 'path', path: href.slice(1)};
131
+ }
132
+
133
+ let nodeIdStr = '';
134
+ if (href.startsWith('#node-')) {
135
+ nodeIdStr = href.replace('#node-', '');
136
+ } else if (href.startsWith('#')) {
137
+ nodeIdStr = href.slice(1);
138
+ }
139
+
140
+ if (nodeIdStr.trim() !== '') {
141
+ const nodeId = Number(nodeIdStr);
142
+ if (Number.isInteger(nodeId)) {
143
+ return {type: 'node', nodeId: nodeId as Protocol.DOM.BackendNodeId};
144
+ }
145
+ }
146
+
147
+ return null;
148
+ }
149
+
150
+ async #linkifyNode(backendNodeId: Protocol.DOM.BackendNodeId, label: string): Promise<Lit.LitTemplate|undefined> {
151
+ const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
152
+ const domModel = target?.model(SDK.DOMModel.DOMModel);
153
+ if (!domModel) {
154
+ return undefined;
155
+ }
156
+ const domNodesMap = await domModel.pushNodesByBackendIdsToFrontend(new Set([backendNodeId]));
157
+ const node = domNodesMap?.get(backendNodeId);
158
+ if (!node) {
159
+ return;
160
+ }
161
+
162
+ if (this.options.mainFrameId && node.frameId() !== this.options.mainFrameId) {
163
+ return;
164
+ }
165
+
166
+ if (!this.#isSameOrigin(node)) {
167
+ return;
168
+ }
169
+
170
+ const linkedNode = PanelsCommon.DOMLinkifier.Linkifier.instance().linkify(node, {textContent: label});
171
+ return linkedNode;
172
+ }
173
+
174
+ async #linkifyPath(path: string, label: string): Promise<Lit.LitTemplate|undefined> {
175
+ const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
176
+ const domModel = target?.model(SDK.DOMModel.DOMModel);
177
+ if (!domModel) {
178
+ return undefined;
179
+ }
180
+ const nodeId = await domModel.pushNodeByPathToFrontend(path);
181
+ if (!nodeId) {
182
+ return;
183
+ }
184
+ const node = domModel.nodeForId(nodeId);
185
+ if (!node) {
186
+ return;
187
+ }
188
+ if (!this.#isSameOrigin(node)) {
189
+ return;
190
+ }
191
+ const linkedNode = PanelsCommon.DOMLinkifier.Linkifier.instance().linkify(node, {textContent: label});
192
+ return linkedNode;
193
+ }
194
+
195
+ override templateForToken(token: Marked.Marked.MarkedToken): Lit.LitTemplate|null {
196
+ if (token.type === 'link') {
197
+ const link = this.#renderLink(token.href, token.text);
198
+ if (link) {
199
+ return link;
200
+ }
201
+ }
202
+
203
+ if (token.type === 'code') {
204
+ const lines = (token.text).split('\n');
205
+ if (lines[0]?.trim() === 'css') {
206
+ token.lang = 'css';
207
+ token.text = lines.slice(1).join('\n');
208
+ }
209
+ }
210
+
211
+ if (token.type === 'codespan') {
212
+ // LLM likes outputting the link inside a codespan block.
213
+ // Remove the codespan and render the link directly
214
+ const matches = token.text.match(/^\[(.*)\]\((.+)\)$/);
215
+ if (matches?.[2]) {
216
+ const link = this.#renderLink(
217
+ matches[2],
218
+ matches[1],
219
+ );
220
+ if (link) {
221
+ return link;
222
+ }
223
+ }
224
+ }
225
+
226
+ return super.templateForToken(token);
227
+ }
228
+ }
@@ -14,8 +14,8 @@ import * as SDK from '../../../core/sdk/sdk.js';
14
14
  import type * as Protocol from '../../../generated/protocol.js';
15
15
  import type {
16
16
  AiWidget, BottomUpTreeAiWidget, ComputedStyleAiWidget, CoreVitalsAiWidget, DomTreeAiWidget, LighthouseReportAiWidget,
17
- NetworkRequestGeneralHeadersAiWidget, NetworkRequestsListAiWidget, PerfInsightAiWidget, PerformanceTraceAiWidget,
18
- SourceCodeAiWidget, SourceFileAiWidget, SourceFilesListAiWidget, StylePropertiesAiWidget,
17
+ NetworkRequestGeneralHeadersAiWidget, NetworkRequestsListAiWidget, NetworkTrackAiWidget, PerfInsightAiWidget,
18
+ PerformanceTraceAiWidget, SourceCodeAiWidget, SourceFileAiWidget, SourceFilesListAiWidget, StylePropertiesAiWidget,
19
19
  TimelineEventSummaryAiWidget, TimelineRangeSummaryAiWidget} from '../../../models/ai_assistance/agents/AiAgent.js';
20
20
  import * as AiAssistanceModel from '../../../models/ai_assistance/ai_assistance.js';
21
21
  import * as ComputedStyle from '../../../models/computed_style/computed_style.js';
@@ -231,6 +231,10 @@ const UIStringsNotTranslate = {
231
231
  * @description Accessible label for the reveal button in the performance summary widget.
232
232
  */
233
233
  revealPerformanceSummary: 'Reveal performance summary',
234
+ /**
235
+ * @description Accessible label for the reveal button in the network track widget.
236
+ */
237
+ revealNetworkActivity: 'Reveal network activity',
234
238
  /**
235
239
  * @description Accessible label for the reveal button in the bottom up thread activity widget.
236
240
  */
@@ -295,6 +299,10 @@ const UIStringsNotTranslate = {
295
299
  * @description Title for the performance summary widget.
296
300
  */
297
301
  performanceSummary: 'Performance summary',
302
+ /**
303
+ * @description Title for the network activity summary widget.
304
+ */
305
+ networkActivitySummary: 'Network activity',
298
306
  /**
299
307
  * @description The title of the button that allows exporting the conversation for agents.
300
308
  */
@@ -1658,6 +1666,8 @@ export function getWidgetSignature(widget: AiWidget): string {
1658
1666
  return `${widget.name}:${widget.data.track}:${widget.data.bounds.min}-${widget.data.bounds.max}`;
1659
1667
  case 'BOTTOM_UP_TREE':
1660
1668
  return `${widget.name}:${widget.data.bounds.min}-${widget.data.bounds.max}`;
1669
+ case 'NETWORK_TRACK':
1670
+ return `${widget.name}:${widget.data.bounds.min}-${widget.data.bounds.max}`;
1661
1671
  case 'SOURCE_FILE':
1662
1672
  return `${widget.name}:${widget.data.uiSourceCode.url()}`;
1663
1673
  case 'SOURCE_FILES_LIST':
@@ -1755,6 +1765,9 @@ async function renderWidgets(
1755
1765
  case 'BOTTOM_UP_TREE':
1756
1766
  response = await makeBottomUpTimelineTreeWidget(widgetData);
1757
1767
  break;
1768
+ case 'NETWORK_TRACK':
1769
+ response = await makeNetworkTrackWidget(widgetData);
1770
+ break;
1758
1771
  case 'SOURCE_FILE':
1759
1772
  response = await makeSourceFileWidget(widgetData);
1760
1773
  break;
@@ -2316,6 +2329,30 @@ async function makeTimelineRangeSummaryWidget(widgetData: TimelineRangeSummaryAi
2316
2329
  };
2317
2330
  }
2318
2331
 
2332
+ async function makeNetworkTrackWidget(widgetData: NetworkTrackAiWidget): Promise<WidgetMakerResponse|null> {
2333
+ const {parsedTrace, bounds} = widgetData.data;
2334
+ const dataProvider = new Timeline.TimelineFlameChartNetworkDataProvider.TimelineFlameChartNetworkDataProvider();
2335
+
2336
+ // clang-format off
2337
+ const template = html`
2338
+ <devtools-performance-agent-network-track
2339
+ .data=${{
2340
+ parsedTrace,
2341
+ bounds,
2342
+ dataProvider,
2343
+ } as TimelineComponents.NetworkTrackWidget.NetworkTrackWidgetData}
2344
+ ></devtools-performance-agent-network-track>`;
2345
+ // clang-format on
2346
+
2347
+ return {
2348
+ renderedWidget: template,
2349
+ revealable: new TimelineUtils.Helpers.RevealableTimeRange(bounds),
2350
+ accessibleRevealLabel: lockedString(UIStringsNotTranslate.revealNetworkActivity),
2351
+ title: lockedString(UIStringsNotTranslate.networkActivitySummary),
2352
+ jslogContext: 'network-track-widget',
2353
+ };
2354
+ }
2355
+
2319
2356
  async function makeLighthouseReportWidget(widgetData: LighthouseReportAiWidget): Promise<WidgetMakerResponse|null> {
2320
2357
  const reportEl =
2321
2358
  Lighthouse.LighthouseReportRenderer.LighthouseReportRenderer.renderLighthouseScores(widgetData.data.report);
@@ -1797,6 +1797,8 @@ export class DOMStorageTreeElement extends ApplicationPanelTreeElement {
1797
1797
  super.onselect(selectedByUser);
1798
1798
  UI.UIUserMetrics.UIUserMetrics.instance().panelShown('dom-storage');
1799
1799
  this.resourcesPanel.showDOMStorage(this.domStorage);
1800
+ const storageItem = this.#getStorageItem();
1801
+ UI.Context.Context.instance().setFlavor(AiAssistance.StorageItem.StorageItem, storageItem);
1800
1802
  return false;
1801
1803
  }
1802
1804
 
@@ -1977,6 +1979,8 @@ export class CookieTreeElement extends ApplicationPanelTreeElement {
1977
1979
  this.resourcesPanel.showCookies(this.target, this.#cookieDomain);
1978
1980
  UI.UIUserMetrics.UIUserMetrics.instance().panelShown(
1979
1981
  Host.UserMetrics.PanelCodes[Host.UserMetrics.PanelCodes.cookies]);
1982
+ const storageItem = this.#getStorageItem();
1983
+ UI.Context.Context.instance().setFlavor(AiAssistance.StorageItem.StorageItem, storageItem);
1980
1984
  return false;
1981
1985
  }
1982
1986
  }
@@ -50,6 +50,14 @@ const UIStrings = {
50
50
  * @description Label for checkbox to show URL-decoded cookie values
51
51
  */
52
52
  showUrlDecoded: 'Show URL-decoded',
53
+ /**
54
+ * @description Text of a context menu item to start a chat with AI
55
+ */
56
+ startAChat: 'Start a chat',
57
+ /**
58
+ * @description Text of a context menu item to explain a web cookie with AI
59
+ */
60
+ explainCookie: 'Explain this cookie',
53
61
  /**
54
62
  * @description Text in Cookie Items View of the Application panel to indicate that no cookie has been selected for preview
55
63
  */
@@ -174,6 +182,10 @@ interface CookieItemsViewInput {
174
182
  onDeleteSelectedItems: () => void;
175
183
  onDeleteAllItems: () => void;
176
184
  onRefreshItems: () => void;
185
+ aiButtonIsEnabled: boolean;
186
+ onPopulateAiContextMenu: (cookie: SDK.Cookie.Cookie, contextMenu: UI.ContextMenu.ContextMenu) => void;
187
+ onAiButtonClick: (cookie: SDK.Cookie.Cookie, event: Event) => void;
188
+ aiButtonTitle?: string;
177
189
  selectedCookie: SDK.Cookie.Cookie|null;
178
190
  }
179
191
 
@@ -203,6 +215,10 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
203
215
  refreshCallback: input.onRefresh,
204
216
  selectedCallback: input.onSelect,
205
217
  deleteCallback: input.onDelete,
218
+ aiButtonIsEnabled: input.aiButtonIsEnabled,
219
+ onAiButtonClick: input.onAiButtonClick,
220
+ onPopulateAiContextMenu: input.onPopulateAiContextMenu,
221
+ aiButtonTitle: input.aiButtonTitle,
206
222
  editable: true,
207
223
  })}
208
224
  ></devtools-widget>
@@ -220,8 +236,8 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
220
236
  </devtools-split-view>
221
237
  </devtools-widget>
222
238
  `,
223
- // clang-format on
224
- target, {container: {attributes: {jslog: `${VisualLogging.pane('cookies-data')}`}}});
239
+ // clang-format on
240
+ target, {container: {attributes: {jslog: `${VisualLogging.pane('cookies-data')}`}}});
225
241
  };
226
242
 
227
243
  export class CookieItemsView extends UI.Widget.VBox {
@@ -261,7 +277,6 @@ export class CookieItemsView extends UI.Widget.VBox {
261
277
  this.cookieDomain = domain;
262
278
  this.refreshItems();
263
279
  this.model.addEventListener(SDK.CookieModel.Events.COOKIE_LIST_UPDATED, this.onCookieListUpdate, this);
264
- this.updateAiAssistanceContext(null);
265
280
  }
266
281
 
267
282
  override performUpdate(): void {
@@ -296,6 +311,12 @@ export class CookieItemsView extends UI.Widget.VBox {
296
311
  onDeleteAllItems: this.deleteAllItems.bind(this),
297
312
  onRefreshItems: this.refreshItems.bind(this),
298
313
  selectedCookie: this.selectedCookie,
314
+ aiButtonIsEnabled: this.isAiButtonEnabled(),
315
+ onPopulateAiContextMenu: this.#onPopulateAiContextMenu.bind(this),
316
+ onAiButtonClick: this.#onAiButtonClick.bind(this),
317
+ aiButtonTitle: this.isAiButtonEnabled() ?
318
+ UI.ActionRegistry.ActionRegistry.instance().getAction('ai-assistance.storage-floating-button').title() :
319
+ undefined,
299
320
  };
300
321
 
301
322
  this.view(input, output, this.contentElement);
@@ -304,7 +325,6 @@ export class CookieItemsView extends UI.Widget.VBox {
304
325
  override wasShown(): void {
305
326
  super.wasShown();
306
327
  this.refreshItems();
307
- this.updateAiAssistanceContext(this.selectedCookie);
308
328
  }
309
329
 
310
330
  private showPreview(cookie: SDK.Cookie.Cookie|null): void {
@@ -313,10 +333,9 @@ export class CookieItemsView extends UI.Widget.VBox {
313
333
  }
314
334
  this.selectedCookie = cookie;
315
335
  this.requestUpdate();
316
- this.updateAiAssistanceContext(cookie);
317
336
  }
318
337
 
319
- private updateAiAssistanceContext(cookie: SDK.Cookie.Cookie|null): void {
338
+ #updateAiAssistanceContext(cookie: SDK.Cookie.Cookie|null): void {
320
339
  if (cookie && cookie.httpOnly()) {
321
340
  UI.Context.Context.instance().setFlavor(AiAssistanceModel.StorageItem.StorageItem, null);
322
341
  return;
@@ -342,6 +361,7 @@ export class CookieItemsView extends UI.Widget.VBox {
342
361
  }
343
362
  this.#toolbar.setCanDeleteSelected(Boolean(selectedCookie));
344
363
  this.showPreview(selectedCookie);
364
+ this.#updateAiAssistanceContext(selectedCookie);
345
365
  }
346
366
 
347
367
  private async saveCookie(newCookie: SDK.Cookie.Cookie, oldCookie: SDK.Cookie.Cookie|null): Promise<boolean> {
@@ -393,6 +413,7 @@ export class CookieItemsView extends UI.Widget.VBox {
393
413
  * This will only delete the currently visible cookies.
394
414
  */
395
415
  deleteAllItems(): void {
416
+ UI.Context.Context.instance().setFlavor(AiAssistanceModel.StorageItem.StorageItem, null);
396
417
  this.showPreview(null);
397
418
  void this.model.deleteCookies(this.shownCookies);
398
419
  }
@@ -412,4 +433,32 @@ export class CookieItemsView extends UI.Widget.VBox {
412
433
  refreshItems(): void {
413
434
  void this.model.getCookiesForDomain(this.cookieDomain, true).then(this.updateWithCookies.bind(this));
414
435
  }
436
+
437
+ private isAiButtonEnabled(): boolean {
438
+ return UI.ActionRegistry.ActionRegistry.instance().hasAction('ai-assistance.storage-floating-button');
439
+ }
440
+
441
+ #onPopulateAiContextMenu(cookie: SDK.Cookie.Cookie, contextMenu: UI.ContextMenu.ContextMenu): void {
442
+ const openAiAssistanceId = 'ai-assistance.application-panel-context';
443
+ if (this.isAiButtonEnabled() && UI.ActionRegistry.ActionRegistry.instance().hasAction(openAiAssistanceId)) {
444
+ this.#updateAiAssistanceContext(cookie);
445
+ if (UI.Context.Context.instance().flavor(AiAssistanceModel.StorageItem.StorageItem)) {
446
+ const action = UI.ActionRegistry.ActionRegistry.instance().getAction(openAiAssistanceId);
447
+ const submenu = contextMenu.footerSection().appendSubMenuItem(action.title(), false, openAiAssistanceId);
448
+ submenu.defaultSection().appendAction(openAiAssistanceId, i18nString(UIStrings.startAChat));
449
+ submenu.defaultSection().appendItem(
450
+ i18nString(UIStrings.explainCookie), () => action.execute({prompt: 'What is the purpose of this cookie?'}),
451
+ {disabled: !action.enabled(), jslogContext: openAiAssistanceId + '.cookies'});
452
+ }
453
+ }
454
+ }
455
+
456
+ #onAiButtonClick(cookie: SDK.Cookie.Cookie, _event: Event): void {
457
+ this.#updateAiAssistanceContext(cookie);
458
+ const actionRegistry = UI.ActionRegistry.ActionRegistry.instance();
459
+ const storageFloatingButtonId = 'ai-assistance.storage-floating-button';
460
+ if (actionRegistry.hasAction(storageFloatingButtonId)) {
461
+ void actionRegistry.getAction(storageFloatingButtonId).execute();
462
+ }
463
+ }
415
464
  }
@@ -55,6 +55,14 @@ const UIStrings = {
55
55
  * @description Text for announcing a DOM Storage key/value item has been deleted
56
56
  */
57
57
  domStorageItemDeleted: 'The storage item was deleted.',
58
+ /**
59
+ * @description Text of a context menu item to start a chat with AI
60
+ */
61
+ startAChat: 'Start a chat',
62
+ /**
63
+ * @description Text of a context menu item to explain a storage item of a storage bucket with AI
64
+ */
65
+ explainItem: 'Explain this item',
58
66
  } as const;
59
67
  const str_ = i18n.i18n.registerUIStrings('panels/application/DOMStorageItemsView.ts', UIStrings);
60
68
  const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
@@ -111,7 +119,6 @@ export class DOMStorageItemsView extends KeyValueStorageItemsView {
111
119
  SDK.DOMStorageModel.DOMStorage.Events.DOM_STORAGE_ITEM_UPDATED, this.domStorageItemUpdated, this),
112
120
  ];
113
121
  this.refreshItems();
114
- this.selectedItemChanged(null);
115
122
  }
116
123
 
117
124
  private domStorageItemsCleared(): void {
@@ -174,13 +181,7 @@ export class DOMStorageItemsView extends KeyValueStorageItemsView {
174
181
  this.showItems(filteredItems);
175
182
  }
176
183
 
177
- override deleteAllItems(): void {
178
- this.domStorage.clear();
179
- // explicitly clear the view because the event won't be fired when it has no items
180
- this.domStorageItemsCleared();
181
- }
182
-
183
- protected override selectedItemChanged(item: {key: string, value: string}|null): void {
184
+ #setAiStorageContext(item: {key: string, value: string}|null): void {
184
185
  const storageKey = this.domStorage.storageKey;
185
186
  if (!storageKey) {
186
187
  return;
@@ -205,10 +206,44 @@ export class DOMStorageItemsView extends KeyValueStorageItemsView {
205
206
  UI.Context.Context.instance().setFlavor(AiAssistanceModel.StorageItem.StorageItem, storageItem);
206
207
  }
207
208
 
209
+ override deleteAllItems(): void {
210
+ this.domStorage.clear();
211
+ // explicitly clear the view because the event won't be fired when it has no items
212
+ this.domStorageItemsCleared();
213
+ }
214
+
215
+ protected override selectedItemChanged(item: {key: string, value: string}|null): void {
216
+ this.#setAiStorageContext(item);
217
+ }
218
+
208
219
  protected override isAiButtonEnabled(): boolean {
209
220
  return UI.ActionRegistry.ActionRegistry.instance().hasAction('ai-assistance.storage-floating-button');
210
221
  }
211
222
 
223
+ protected override populateContextMenu(item: {key: string, value: string},
224
+ contextMenu: UI.ContextMenu.ContextMenu): void {
225
+ const openAiAssistanceId = 'ai-assistance.application-panel-context';
226
+ const actionRegistry = UI.ActionRegistry.ActionRegistry.instance();
227
+ if (actionRegistry.hasAction(openAiAssistanceId)) {
228
+ this.#setAiStorageContext(item);
229
+ const action = actionRegistry.getAction(openAiAssistanceId);
230
+ const submenu = contextMenu.footerSection().appendSubMenuItem(action.title(), false, openAiAssistanceId);
231
+ submenu.defaultSection().appendAction(openAiAssistanceId, i18nString(UIStrings.startAChat));
232
+ submenu.defaultSection().appendItem(i18nString(UIStrings.explainItem),
233
+ () => action.execute({prompt: 'Explain this storage item.'}),
234
+ {disabled: !action.enabled(), jslogContext: openAiAssistanceId + '.storage'});
235
+ }
236
+ }
237
+
238
+ protected override onAiButtonClick(item: {key: string, value: string}, _event: Event): void {
239
+ this.#setAiStorageContext(item);
240
+ const aiFloatingActionId = 'ai-assistance.storage-floating-button';
241
+ const actionRegistry = UI.ActionRegistry.ActionRegistry.instance();
242
+ if (actionRegistry.hasAction(aiFloatingActionId)) {
243
+ void actionRegistry.getAction(aiFloatingActionId).execute();
244
+ }
245
+ }
246
+
212
247
  protected removeItem(key: string): void {
213
248
  this.domStorage?.removeItem(key);
214
249
  }
@@ -92,6 +92,7 @@ export interface ViewInput {
92
92
  onDelete: (key: string) => void;
93
93
  onDeleteSelected: () => void;
94
94
  onDeleteAll: () => void;
95
+ onContextMenu?: (item: {key: string, value: string}, contextMenu: UI.ContextMenu.ContextMenu) => void;
95
96
  jslog?: string;
96
97
  classes?: string[];
97
98
  aiButtonTitle?: string;
@@ -175,6 +176,7 @@ export abstract class KeyValueStorageItemsView extends UI.Widget.VBox {
175
176
  @edit=${(e: CustomEvent<{columnId: string, valueBeforeEditing: string, newText: string}>) =>
176
177
  input.onEdit(item.key, item.value, e.detail.columnId, e.detail.valueBeforeEditing, e.detail.newText)}
177
178
  @delete=${() => input.onDelete(item.key)}
179
+ @contextmenu=${(e: CustomEvent<UI.ContextMenu.ContextMenu>) => input.onContextMenu?.(item, e.detail)}
178
180
  selected=${(input.selectedKey === item.key) || nothing}>
179
181
  <td>${input.showAiButton ? html`
180
182
  <span class="ai-button-container">
@@ -198,8 +200,8 @@ export abstract class KeyValueStorageItemsView extends UI.Widget.VBox {
198
200
  ${input.preview?.element}
199
201
  </devtools-widget>
200
202
  </devtools-split-view>`,
201
- // clang-format on
202
- target, {container: {attributes: {jslog: input.jslog}, classes: input.classes}});
203
+ // clang-format on
204
+ target, {container: {attributes: {jslog: input.jslog}, classes: input.classes}});
203
205
  };
204
206
  }
205
207
  super();
@@ -246,13 +248,13 @@ export abstract class KeyValueStorageItemsView extends UI.Widget.VBox {
246
248
  void this.#previewEntry(item);
247
249
  this.selectedItemChanged(item);
248
250
  },
249
- onAiButtonClick: (item: {key: string, value: string}, event: Event) => {
250
- event.stopPropagation();
251
- viewInput.onSelect(item);
252
- const actionRegistry = UI.ActionRegistry.ActionRegistry.instance();
253
- if (actionRegistry.hasAction(STORAGE_FLOATING_BUTTON_ACTION_ID)) {
254
- void actionRegistry.getAction(STORAGE_FLOATING_BUTTON_ACTION_ID).execute();
255
- }
251
+ onAiButtonClick: this.isAiButtonEnabled() ?
252
+ (item: {key: string, value: string}, event: Event) => {
253
+ this.onAiButtonClick(item, event);
254
+ } :
255
+ undefined,
256
+ onContextMenu: (item: {key: string, value: string}, contextMenu: UI.ContextMenu.ContextMenu) => {
257
+ this.populateContextMenu(item, contextMenu);
256
258
  },
257
259
 
258
260
  onSort: (ascending: boolean) => {
@@ -284,6 +286,12 @@ export abstract class KeyValueStorageItemsView extends UI.Widget.VBox {
284
286
  return false;
285
287
  }
286
288
 
289
+ protected populateContextMenu(_item: {key: string, value: string}, _contextMenu: UI.ContextMenu.ContextMenu): void {
290
+ }
291
+
292
+ protected onAiButtonClick(_item: {key: string, value: string}, _event: Event): void {
293
+ }
294
+
287
295
  protected get toolbar(): StorageItemsToolbar|undefined {
288
296
  return this.#toolbar;
289
297
  }