Skip to content

Commit 753ccb7

Browse files
ktranDevtools-frontend LUCI CQ
authored andcommitted
Add submenu items to 'Debug with Ai' in Sources Panel
This CL allows us to show submenu items in the context menu, when right clicking on a script file in the Sources Panel. They contain example prompts that directly open and run a prompt in the Ai Assistance panel. Bug: 433468811 Change-Id: I6e38e0c7f1e96e31cb975ea6f83f9340a9db01c4 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6802286 Auto-Submit: Kim-Anh Tran <[email protected]> Reviewed-by: Ergün Erdoğmuş <[email protected]> Commit-Queue: Ergün Erdoğmuş <[email protected]>
1 parent cfd592a commit 753ccb7

File tree

10 files changed

+228
-16
lines changed

10 files changed

+228
-16
lines changed

front_end/panels/ai_assistance/AiAssistancePanel.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,40 @@ describeWithMockConnection('AI Assistance Panel', () => {
654654
]);
655655
});
656656

657+
it('runs action-triggered prompts', async () => {
658+
updateHostConfig({
659+
devToolsFreestyler: {
660+
enabled: true,
661+
},
662+
});
663+
664+
const {panel, view} = await createAiAssistancePanel({
665+
aidaClient: mockAidaClient(
666+
[
667+
[{explanation: 'test'}],
668+
],
669+
),
670+
});
671+
672+
panel.handleAction('freestyler.element-panel-context', {prompt: 'Tell me more'});
673+
assert.deepEqual((await view.nextInput).messages, [
674+
{
675+
entity: AiAssistancePanel.ChatMessageEntity.USER,
676+
text: 'Tell me more',
677+
imageInput: undefined,
678+
},
679+
{
680+
answer: 'test',
681+
entity: AiAssistancePanel.ChatMessageEntity.MODEL,
682+
rpcId: undefined,
683+
suggestions: undefined,
684+
steps: [],
685+
},
686+
]);
687+
});
688+
657689
it('should not save partial responses to conversation history', async () => {
690+
658691
updateHostConfig({
659692
devToolsFreestyler: {
660693
enabled: true,
@@ -993,6 +1026,33 @@ describeWithMockConnection('AI Assistance Panel', () => {
9931026
assert.isTrue((await view.nextInput).blockedByCrossOrigin);
9941027
assert.strictEqual(view.input.selectedContext?.getItem(), networkRequest2);
9951028
});
1029+
1030+
it('starts a new chat when a predefined prompt for a cross origin request is sent', async () => {
1031+
const networkRequest = createNetworkRequest({
1032+
url: urlString`https://a.test`,
1033+
});
1034+
UI.Context.Context.instance().setFlavor(SDK.NetworkRequest.NetworkRequest, networkRequest);
1035+
1036+
const {panel, view} = await createAiAssistancePanel({
1037+
aidaClient: mockAidaClient([
1038+
[{explanation: 'test'}],
1039+
])
1040+
});
1041+
panel.handleAction('drjones.network-floating-button', {prompt: 'Tell me more'});
1042+
assert.isFalse((await view.nextInput).blockedByCrossOrigin);
1043+
1044+
// Change context to https://b.test.
1045+
const networkRequest2 = createNetworkRequest({
1046+
url: urlString`https://b.test`,
1047+
});
1048+
UI.Context.Context.instance().setFlavor(SDK.NetworkRequest.NetworkRequest, networkRequest2);
1049+
1050+
// A predefined prompt from the user on a different origin has been initiated.
1051+
// This should automatically start a new chat, to allow for the prompt to be executed.
1052+
panel.handleAction('drjones.network-floating-button', {prompt: 'Tell me more about another one'});
1053+
const input = await view.nextInput;
1054+
assert.isFalse(input.blockedByCrossOrigin);
1055+
});
9961056
});
9971057

9981058
describe('auto agent selection for panels', () => {

front_end/panels/ai_assistance/AiAssistancePanel.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,7 +1163,7 @@ export class AiAssistancePanel extends UI.Panel.Panel {
11631163
// Node picker is using linkifier.
11641164
}
11651165

1166-
handleAction(actionId: string): void {
1166+
handleAction(actionId: string, opts?: Record<string, unknown>): void {
11671167
if (this.#isLoading) {
11681168
// If running some queries already, focus the input with the abort
11691169
// button and do nothing.
@@ -1227,7 +1227,18 @@ export class AiAssistancePanel extends UI.Panel.Panel {
12271227
agent = this.#createAgent(targetConversationType);
12281228
}
12291229
this.#updateConversationState(agent);
1230-
this.#viewOutput.chatView?.focusTextInput();
1230+
const predefinedPrompt = opts?.['prompt'];
1231+
if (predefinedPrompt && typeof predefinedPrompt === 'string') {
1232+
this.#imageInput = undefined;
1233+
this.#isTextInputEmpty = true;
1234+
Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiAssistanceQuerySubmitted);
1235+
if (this.#blockedByCrossOrigin) {
1236+
this.#handleNewChatRequest();
1237+
}
1238+
void this.#startConversation(predefinedPrompt);
1239+
} else {
1240+
this.#viewOutput.chatView?.focusTextInput();
1241+
}
12311242
}
12321243

12331244
#populateHistoryMenu(contextMenu: UI.ContextMenu.ContextMenu): void {
@@ -1877,10 +1888,7 @@ export class AiAssistancePanel extends UI.Panel.Panel {
18771888
}
18781889

18791890
export class ActionDelegate implements UI.ActionRegistration.ActionDelegate {
1880-
handleAction(
1881-
_context: UI.Context.Context,
1882-
actionId: string,
1883-
): boolean {
1891+
handleAction(_context: UI.Context.Context, actionId: string, opts?: Record<string, unknown>): boolean {
18841892
switch (actionId) {
18851893
case 'freestyler.elements-floating-button':
18861894
case 'freestyler.element-panel-context':
@@ -1911,7 +1919,7 @@ export class ActionDelegate implements UI.ActionRegistration.ActionDelegate {
19111919
}
19121920

19131921
const widget = (await view.widget()) as AiAssistancePanel;
1914-
widget.handleAction(actionId);
1922+
widget.handleAction(actionId, opts);
19151923
})();
19161924
return true;
19171925
}

front_end/panels/ai_assistance/ai_assistance-meta.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ const UIStrings = {
3434
* the current element as context
3535
*/
3636
askAi: 'Ask AI',
37+
/**
38+
*@description Text of a context menu item to redirect to the AI assistance panel with
39+
* the current context
40+
*/
41+
debugWithAi: 'Debug with AI',
3742
/**
3843
* @description Message shown to the user if the DevTools locale is not
3944
* supported.
@@ -258,7 +263,7 @@ UI.ActionRegistration.registerActionExtension({
258263
return [];
259264
},
260265
category: UI.ActionRegistration.ActionCategory.GLOBAL,
261-
title: i18nLazyString(UIStrings.askAi),
266+
title: i18nLazyString(UIStrings.debugWithAi),
262267
async loadActionDelegate() {
263268
const AiAssistance = await loadAiAssistanceModule();
264269
return new AiAssistance.ActionDelegate();

front_end/panels/sources/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ ts_library("unittests") {
160160
"OutlineQuickOpen.test.ts",
161161
"ResourceOriginPlugin.test.ts",
162162
"SourcesNavigator.test.ts",
163+
"SourcesPanel.test.ts",
163164
"SourcesView.test.ts",
164165
"TabbedEditorContainer.test.ts",
165166
"UISourceCodeFrame.test.ts",
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
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 type * as Platform from '../../core/platform/platform.js';
7+
import * as SDK from '../../core/sdk/sdk.js';
8+
import * as Bindings from '../../models/bindings/bindings.js';
9+
import * as Breakpoints from '../../models/breakpoints/breakpoints.js';
10+
import * as Persistence from '../../models/persistence/persistence.js';
11+
import * as Workspace from '../../models/workspace/workspace.js';
12+
import {describeWithEnvironment, registerNoopActions, updateHostConfig} from '../../testing/EnvironmentHelpers.js';
13+
import * as UI from '../../ui/legacy/legacy.js';
14+
15+
import * as Sources from './sources.js';
16+
17+
describeWithEnvironment('SourcesPanel', () => {
18+
function setUpEnvironment() {
19+
const workspace = Workspace.Workspace.WorkspaceImpl.instance({forceNew: true});
20+
const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({
21+
forceNew: true,
22+
targetManager: SDK.TargetManager.TargetManager.instance(),
23+
resourceMapping:
24+
new Bindings.ResourceMapping.ResourceMapping(SDK.TargetManager.TargetManager.instance(), workspace),
25+
ignoreListManager: Workspace.IgnoreListManager.IgnoreListManager.instance({forceNew: true}),
26+
});
27+
const breakpointManager = Breakpoints.BreakpointManager.BreakpointManager.instance({
28+
forceNew: true,
29+
targetManager: SDK.TargetManager.TargetManager.instance(),
30+
workspace,
31+
debuggerWorkspaceBinding,
32+
});
33+
Persistence.Persistence.PersistenceImpl.instance({forceNew: true, workspace, breakpointManager});
34+
const networkPersistenceManager =
35+
sinon.createStubInstance(Persistence.NetworkPersistenceManager.NetworkPersistenceManager);
36+
sinon.stub(Persistence.NetworkPersistenceManager.NetworkPersistenceManager, 'instance')
37+
.returns(networkPersistenceManager);
38+
sinon.stub(UI.ViewManager.ViewManager.instance(), 'view')
39+
.callsFake(() => sinon.createStubInstance(UI.View.SimpleView));
40+
}
41+
42+
function createStubUISourceCode() {
43+
const uiSourceCode = sinon.createStubInstance(Workspace.UISourceCode.UISourceCode);
44+
uiSourceCode.contentType.returns(Common.ResourceType.resourceTypes.Script);
45+
const stubProject = sinon.createStubInstance(Bindings.ContentProviderBasedProject.ContentProviderBasedProject);
46+
uiSourceCode.project.returns(stubProject);
47+
stubProject.isServiceProject.returns(true);
48+
return uiSourceCode;
49+
}
50+
51+
it('Shows Debug with Ai menu and submenu items', () => {
52+
updateHostConfig({
53+
devToolsAiSubmenuPrompts: {
54+
enabled: true,
55+
},
56+
});
57+
58+
registerNoopActions([
59+
'debugger.toggle-pause', 'debugger.step-over', 'debugger.step-into', 'debugger.step-out', 'debugger.step',
60+
'debugger.toggle-breakpoints-active'
61+
]);
62+
UI.ActionRegistration.registerActionExtension({
63+
actionId: 'drjones.sources-panel-context',
64+
title: () => 'Debug with AI' as Platform.UIString.LocalizedString,
65+
category: UI.ActionRegistration.ActionCategory.GLOBAL,
66+
});
67+
const actionRegistryInstance = UI.ActionRegistry.ActionRegistry.instance({forceNew: true});
68+
UI.ShortcutRegistry.ShortcutRegistry.instance({forceNew: true, actionRegistry: actionRegistryInstance});
69+
70+
setUpEnvironment();
71+
72+
const sources = new Sources.SourcesPanel.SourcesPanel();
73+
74+
const event = new Event('contextmenu');
75+
sinon.stub(event, 'target').value(document);
76+
const contextMenu = new UI.ContextMenu.ContextMenu(event);
77+
78+
const uiSourceCode = createStubUISourceCode();
79+
sources.appendApplicableItems(event, contextMenu, uiSourceCode);
80+
81+
const debugWithAiItem = contextMenu.buildDescriptor().subItems?.find(item => item.label === 'Debug with AI');
82+
assert.exists(debugWithAiItem);
83+
assert.deepEqual(
84+
debugWithAiItem.subItems?.map(item => item.label),
85+
['Start a chat', 'Assess performance', 'Explain this script', 'Explain input handling']);
86+
87+
UI.ActionRegistry.ActionRegistry.reset();
88+
UI.ShortcutRegistry.ShortcutRegistry.removeInstance();
89+
});
90+
});

front_end/panels/sources/SourcesPanel.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,27 @@ const UIStrings = {
167167
*@description Text in Sources Panel of the Sources panel
168168
*/
169169
openInSourcesPanel: 'Open in Sources panel',
170+
/**
171+
*@description Context menu text in Sources Panel to that opens a submenu with AI prompts.
172+
*/
173+
debugWithAi: 'Debug with AI',
174+
/**
175+
*@description Text of a context menu item to redirect to the AI assistance panel and to start a chat.
176+
*/
177+
startAChat: 'Start a chat',
178+
/**
179+
*@description Text of a context menu item to redirect to the AI assistance panel and directly execute
180+
* a prompt to assess the performance of a script.
181+
*/
182+
assessPerformance: 'Assess performance',
183+
/**
184+
*@description Context menu item in Sources panel to explain a script via AI.
185+
*/
186+
explainThisScript: 'Explain this script',
187+
/**
188+
*@description Context menu item in Sources panel to explain input handling in a script via AI.
189+
*/
190+
explainInputHandling: 'Explain input handling',
170191
} as const;
171192
const str_ = i18n.i18n.registerUIStrings('panels/sources/SourcesPanel.ts', UIStrings);
172193
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
@@ -945,11 +966,28 @@ export class SourcesPanel extends UI.Panel.Panel implements
945966
});
946967
}
947968

948-
if (UI.ActionRegistry.ActionRegistry.instance().hasAction('drjones.sources-panel-context')) {
969+
const openAiAssistanceId = 'drjones.sources-panel-context';
970+
if (UI.ActionRegistry.ActionRegistry.instance().hasAction(openAiAssistanceId)) {
949971
const editorElement = this.element.querySelector('devtools-text-editor');
950972
if (!eventTarget.isSelfOrDescendant(editorElement) && uiSourceCode.contentType().isTextType()) {
951973
UI.Context.Context.instance().setFlavor(Workspace.UISourceCode.UISourceCode, uiSourceCode);
952-
contextMenu.footerSection().appendAction('drjones.sources-panel-context');
974+
if (Root.Runtime.hostConfig.devToolsAiSubmenuPrompts?.enabled) {
975+
const action = UI.ActionRegistry.ActionRegistry.instance().getAction(openAiAssistanceId);
976+
const submenu = contextMenu.footerSection().appendSubMenuItem(
977+
i18nString(UIStrings.debugWithAi), false, openAiAssistanceId);
978+
submenu.defaultSection().appendAction('drjones.sources-panel-context', i18nString(UIStrings.startAChat));
979+
appendSubmenuPromptAction(
980+
submenu, action, i18nString(UIStrings.assessPerformance), 'Is this script optimized for performance?',
981+
openAiAssistanceId + '.performance');
982+
appendSubmenuPromptAction(
983+
submenu, action, i18nString(UIStrings.explainThisScript), 'What does this script do?',
984+
openAiAssistanceId + '.script');
985+
appendSubmenuPromptAction(
986+
submenu, action, i18nString(UIStrings.explainInputHandling), 'Does the script handle user input safely',
987+
openAiAssistanceId + '.input');
988+
} else {
989+
contextMenu.footerSection().appendAction(openAiAssistanceId);
990+
}
953991
}
954992
}
955993

@@ -960,6 +998,13 @@ export class SourcesPanel extends UI.Panel.Panel implements
960998
.every(script => script.isJavaScript())) {
961999
this.callstackPane.appendIgnoreListURLContextMenuItems(contextMenu, uiSourceCode);
9621000
}
1001+
1002+
function appendSubmenuPromptAction(
1003+
submenu: UI.ContextMenu.SubMenu, action: UI.ActionRegistration.Action, label: Common.UIString.LocalizedString,
1004+
prompt: string, jslogContext: string): void {
1005+
submenu.defaultSection().appendItem(
1006+
label, () => action.execute({prompt}), {disabled: !action.enabled(), jslogContext});
1007+
}
9631008
}
9641009

9651010
private appendUISourceCodeFrameItems(contextMenu: UI.ContextMenu.ContextMenu, target: UISourceCodeFrame): void {

front_end/ui/legacy/ActionRegistration.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ const str_ = i18n.i18n.registerUIStrings('ui/legacy/ActionRegistration.ts', UISt
9999
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
100100

101101
export interface ActionDelegate {
102-
handleAction(context: Context, actionId: string): boolean;
102+
handleAction(context: Context, actionId: string, opts?: Record<string, unknown>): boolean;
103103
}
104104

105105
export class Action extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
@@ -115,13 +115,13 @@ export class Action extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
115115
return this.actionRegistration.actionId;
116116
}
117117

118-
async execute(): Promise<boolean> {
118+
async execute(opts?: Record<string, unknown>): Promise<boolean> {
119119
if (!this.actionRegistration.loadActionDelegate) {
120120
return false;
121121
}
122122
const delegate = await this.actionRegistration.loadActionDelegate();
123123
const actionId = this.id();
124-
return delegate.handleAction(Context.instance(), actionId);
124+
return delegate.handleAction(Context.instance(), actionId, opts);
125125
}
126126

127127
icon(): string|undefined {

front_end/ui/legacy/ContextMenu.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ describeWithEnvironment('ContextMenu', () => {
155155
const event = new Event('contextmenu');
156156
sinon.stub(event, 'target').value(document);
157157
const contextMenu = new UI.ContextMenu.ContextMenu(event);
158-
contextMenu.defaultSection().appendAction('test-action', 'mockLabel', false, 'mockFeature');
158+
contextMenu.defaultSection().appendAction('test-action', 'mockLabel', false, undefined, 'mockFeature');
159159
await contextMenu.show();
160160
sinon.assert.calledOnce(showContextMenuAtPoint);
161161

front_end/ui/legacy/ContextMenu.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ export class Section {
225225
return item;
226226
}
227227

228-
appendAction(actionId: string, label?: string, optional?: boolean, feature?: string): void {
228+
appendAction(actionId: string, label?: string, optional?: boolean, jslogContext?: string, feature?: string): void {
229229
if (optional && !ActionRegistry.instance().hasAction(actionId)) {
230230
return;
231231
}
@@ -235,7 +235,7 @@ export class Section {
235235
}
236236
const result = this.appendItem(label, action.execute.bind(action), {
237237
disabled: !action.enabled(),
238-
jslogContext: actionId,
238+
jslogContext: jslogContext ?? actionId,
239239
featureName: feature,
240240
});
241241
const shortcut = ShortcutRegistry.instance().shortcutTitleForAction(actionId);

front_end/ui/visual_logging/KnownContextValues.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,6 +1299,9 @@ export const knownContextValues = new Set([
12991299
'drjones.performance-panel-context',
13001300
'drjones.sources-floating-button',
13011301
'drjones.sources-panel-context',
1302+
'drjones.sources-panel-context.input',
1303+
'drjones.sources-panel-context.performance',
1304+
'drjones.sources-panel-context.script',
13021305
'drop',
13031306
'duration',
13041307
'durationchange',

0 commit comments

Comments
 (0)