Skip to content

Commit 4aab825

Browse files
OrKoNDevtools-frontend LUCI CQ
authored andcommitted
[AI Asssitance] Extract Patch widget
Bug: none Change-Id: I25e722e643541ea52ee9f21b3467b5d889f46c9a Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6308809 Reviewed-by: Wolfgang Beyer <[email protected]> Commit-Queue: Alex Rudenko <[email protected]>
1 parent 3c3b6ca commit 4aab825

File tree

9 files changed

+368
-253
lines changed

9 files changed

+368
-253
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,6 +1202,7 @@ grd_files_debug_sources = [
12021202
"front_end/panels/ai_assistance/ChangeManager.js",
12031203
"front_end/panels/ai_assistance/EvaluateAction.js",
12041204
"front_end/panels/ai_assistance/ExtensionScope.js",
1205+
"front_end/panels/ai_assistance/PatchWidget.js",
12051206
"front_end/panels/ai_assistance/agents/AiAgent.js",
12061207
"front_end/panels/ai_assistance/agents/FileAgent.js",
12071208
"front_end/panels/ai_assistance/agents/NetworkAgent.js",

front_end/panels/ai_assistance/AiAssistancePanel.test.ts

Lines changed: 2 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,16 @@ import * as Platform from '../../core/platform/platform.js';
88
import * as SDK from '../../core/sdk/sdk.js';
99
import * as Workspace from '../../models/workspace/workspace.js';
1010
import {
11+
cleanup,
1112
createAiAssistancePanel,
1213
createNetworkRequest,
13-
detachPanels,
1414
mockAidaClient
1515
} from '../../testing/AiAssistanceHelpers.js';
1616
import {findMenuItemWithLabel, getMenu} from '../../testing/ContextMenuHelpers.js';
1717
import {createTarget, registerNoopActions, updateHostConfig} from '../../testing/EnvironmentHelpers.js';
1818
import {expectCall} from '../../testing/ExpectStubCall.js';
1919
import {describeWithMockConnection} from '../../testing/MockConnection.js';
2020
import {createNetworkPanelForMockConnection} from '../../testing/NetworkHelpers.js';
21-
import {createFileSystemUISourceCode} from '../../testing/UISourceCodeHelpers.js';
2221
import * as UI from '../../ui/legacy/legacy.js';
2322
import * as Elements from '../elements/elements.js';
2423
import * as Network from '../network/network.js';
@@ -44,7 +43,7 @@ describeWithMockConnection('AI Assistance Panel', () => {
4443
});
4544

4645
afterEach(() => {
47-
detachPanels();
46+
cleanup();
4847
});
4948

5049
describe('consent view', () => {
@@ -1263,62 +1262,4 @@ describeWithMockConnection('AI Assistance Panel', () => {
12631262
]);
12641263
});
12651264
});
1266-
1267-
describe('workspace', () => {
1268-
function createTestFilesystem(fileSystemPath: string) {
1269-
const {project, uiSourceCode} = createFileSystemUISourceCode({
1270-
url: Platform.DevToolsPath.urlString`file:///example.html`,
1271-
mimeType: 'text/html',
1272-
content: 'content',
1273-
fileSystemPath,
1274-
});
1275-
return {project, uiSourceCode};
1276-
}
1277-
1278-
it('does not report a workspace project if disabled', async () => {
1279-
createTestFilesystem('file://test');
1280-
updateHostConfig({
1281-
devToolsFreestyler: {
1282-
enabled: true,
1283-
patching: false,
1284-
},
1285-
});
1286-
const {
1287-
initialViewInput,
1288-
} = await createAiAssistancePanel();
1289-
assert.strictEqual(initialViewInput.projectName, '');
1290-
});
1291-
1292-
it('reports a current workspace project', async () => {
1293-
createTestFilesystem('file://test');
1294-
updateHostConfig({
1295-
devToolsFreestyler: {
1296-
enabled: true,
1297-
patching: true,
1298-
},
1299-
});
1300-
const {
1301-
initialViewInput,
1302-
} = await createAiAssistancePanel();
1303-
assert.strictEqual(initialViewInput.projectName, 'test');
1304-
});
1305-
1306-
it('reports an updated project', async () => {
1307-
const {project} = createTestFilesystem('file://test');
1308-
updateHostConfig({
1309-
devToolsFreestyler: {
1310-
enabled: true,
1311-
patching: true,
1312-
},
1313-
});
1314-
const {initialViewInput, expectViewUpdate} = await createAiAssistancePanel();
1315-
assert.strictEqual(initialViewInput.projectName, 'test');
1316-
1317-
const updatedViewInput = await expectViewUpdate(() => {
1318-
Workspace.Workspace.WorkspaceImpl.instance().removeProject(project);
1319-
createTestFilesystem('file://test2');
1320-
});
1321-
assert.strictEqual(updatedViewInput.projectName, 'test2');
1322-
});
1323-
});
13241265
});

front_end/panels/ai_assistance/AiAssistancePanel.ts

Lines changed: 1 addition & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import type * as Platform from '../../core/platform/platform.js';
1111
import * as Root from '../../core/root/root.js';
1212
import * as SDK from '../../core/sdk/sdk.js';
1313
import * as Protocol from '../../generated/protocol.js';
14-
import * as Persistence from '../../models/persistence/persistence.js';
1514
import * as Workspace from '../../models/workspace/workspace.js';
1615
import * as Buttons from '../../ui/components/buttons/buttons.js';
1716
import * as UI from '../../ui/legacy/legacy.js';
@@ -40,7 +39,6 @@ import {
4039
NetworkAgent,
4140
RequestContext,
4241
} from './agents/NetworkAgent.js';
43-
import {PatchAgent} from './agents/PatchAgent.js';
4442
import {CallTreeContext, PerformanceAgent} from './agents/PerformanceAgent.js';
4543
import {InsightContext, PerformanceInsightsAgent} from './agents/PerformanceInsightsAgent.js';
4644
import {NodeContext, StylingAgent, StylingAgentWithFunctionCalling} from './agents/StylingAgent.js';
@@ -56,6 +54,7 @@ import {
5654
State as ChatViewState,
5755
type Step
5856
} from './components/ChatView.js';
57+
import {isAiAssistancePatchingEnabled} from './PatchWidget.js';
5958

6059
const {html} = Lit;
6160

@@ -441,11 +440,7 @@ export class AiAssistancePanel extends UI.Panel.Panel {
441440
accountImage?: string,
442441
accountFullName?: string,
443442
};
444-
#project?: Workspace.Workspace.Project;
445-
#patchSuggestion?: string;
446-
#patchSuggestionLoading?: boolean;
447443
#imageInput = '';
448-
#workspace = Workspace.Workspace.WorkspaceImpl.instance();
449444

450445
constructor(private view: View = defaultView, {aidaClient, aidaAvailability, syncInfo}: {
451446
aidaClient: Host.AidaClient.AidaClient,
@@ -468,36 +463,6 @@ export class AiAssistancePanel extends UI.Panel.Panel {
468463
this.#historicalConversations = AiHistoryStorage.instance().getHistory().map(item => {
469464
return new Conversation(item.type, item.history, item.id, true);
470465
});
471-
472-
this.#selectProject();
473-
}
474-
475-
#selectProject(): void {
476-
if (isAiAssistancePatchingEnabled()) {
477-
// TODO: this is temporary code that should be replaced with
478-
// workflow selection flow. For now it picks the first Workspace
479-
// project that is not Snippets.
480-
const projects = this.#workspace.projectsForType(Workspace.Workspace.projectTypes.FileSystem);
481-
this.#project = undefined;
482-
for (const project of projects) {
483-
// This is for TypeScript to narrow the types. projectsForType()
484-
// probably only returns instances of
485-
// Persistence.FileSystemWorkspaceBinding.FileSystem.
486-
if (!(project instanceof Persistence.FileSystemWorkspaceBinding.FileSystem)) {
487-
continue;
488-
}
489-
if (project.fileSystem().type() !== Persistence.PlatformFileSystem.PlatformFileSystemType.WORKSPACE_PROJECT) {
490-
continue;
491-
}
492-
this.#project = project;
493-
this.requestUpdate();
494-
break;
495-
}
496-
}
497-
}
498-
499-
#onProjectAddedOrRemoved(): void {
500-
this.#selectProject();
501466
}
502467

503468
#getChatUiState(): ChatViewState {
@@ -702,16 +667,6 @@ export class AiAssistancePanel extends UI.Panel.Panel {
702667
SDK.TargetManager.TargetManager.instance().addModelListener(
703668
SDK.DOMModel.DOMModel, SDK.DOMModel.Events.AttrRemoved, this.#handleDOMNodeAttrChange, this);
704669
Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiAssistancePanelOpened);
705-
706-
if (isAiAssistancePatchingEnabled()) {
707-
this.#workspace.addEventListener(Workspace.Workspace.Events.ProjectAdded, this.#onProjectAddedOrRemoved, this);
708-
this.#workspace.addEventListener(Workspace.Workspace.Events.ProjectRemoved, this.#onProjectAddedOrRemoved, this);
709-
710-
// @ts-expect-error temporary global function for local testing.
711-
window.aiAssistanceTestPatchPrompt = async (changeSummary: string) => {
712-
return await this.#applyPatch(changeSummary);
713-
};
714-
}
715670
}
716671

717672
override willHide(): void {
@@ -748,12 +703,6 @@ export class AiAssistancePanel extends UI.Panel.Panel {
748703
this.#handleDOMNodeAttrChange,
749704
this,
750705
);
751-
752-
if (isAiAssistancePatchingEnabled()) {
753-
this.#workspace.removeEventListener(Workspace.Workspace.Events.ProjectAdded, this.#onProjectAddedOrRemoved, this);
754-
this.#workspace.removeEventListener(
755-
Workspace.Workspace.Events.ProjectRemoved, this.#onProjectAddedOrRemoved, this);
756-
}
757706
}
758707

759708
#handleAidaAvailabilityChange = async(): Promise<void> => {
@@ -849,15 +798,12 @@ export class AiAssistancePanel extends UI.Panel.Panel {
849798
conversationType: this.#conversation?.type,
850799
isReadOnly: this.#conversation?.isReadOnly ?? false,
851800
changeSummary: this.#getChangeSummary(),
852-
patchSuggestion: this.#patchSuggestion,
853-
patchSuggestionLoading: this.#patchSuggestionLoading,
854801
inspectElementToggled: this.#toggleSearchElementAction.toggled(),
855802
userInfo: this.#userInfo,
856803
canShowFeedbackForm: this.#serverSideLoggingEnabled,
857804
multimodalInputEnabled:
858805
isAiAssistanceMultimodalInputEnabled() && this.#conversation?.type === ConversationType.STYLING,
859806
imageInput: this.#imageInput,
860-
projectName: this.#project?.displayName() ?? '',
861807
isDeleteHistoryButtonVisible: Boolean(this.#conversation && !this.#conversation.isEmpty),
862808
isTextInputDisabled: this.#isTextInputDisabled(),
863809
emptyStateSuggestions: this.#conversation ? getEmptyStateSuggestions(this.#conversation.type) : [],
@@ -885,7 +831,6 @@ export class AiAssistancePanel extends UI.Panel.Panel {
885831
onTakeScreenshot: isAiAssistanceMultimodalInputEnabled() ? this.#handleTakeScreenshot.bind(this) : undefined,
886832
onRemoveImageInput: isAiAssistanceMultimodalInputEnabled() ? this.#handleRemoveImageInput.bind(this) :
887833
undefined,
888-
onApplyToWorkspace: this.#onApplyToWorkspace.bind(this)
889834
},
890835
this.#viewOutput, this.contentElement);
891836
}
@@ -1263,36 +1208,6 @@ export class AiAssistancePanel extends UI.Panel.Panel {
12631208
UI.ARIAUtils.alert(lockedString(UIStringsNotTranslate.answerReady));
12641209
}
12651210

1266-
async #onApplyToWorkspace(): Promise<void> {
1267-
if (!isAiAssistancePatchingEnabled()) {
1268-
return;
1269-
}
1270-
const changeSummary = this.#getChangeSummary();
1271-
if (!changeSummary) {
1272-
throw new Error('Change summary does not exist');
1273-
}
1274-
1275-
this.#patchSuggestionLoading = true;
1276-
this.requestUpdate();
1277-
const response = await this.#applyPatch(changeSummary);
1278-
this.#patchSuggestion = response?.type === ResponseType.ANSWER ? response.text : 'Could not update files';
1279-
this.#patchSuggestionLoading = false;
1280-
this.requestUpdate();
1281-
}
1282-
1283-
async #applyPatch(changeSummary: string): Promise<ResponseData|undefined> {
1284-
if (!this.#project) {
1285-
throw new Error('Project does not exist');
1286-
}
1287-
const agent = new PatchAgent({
1288-
aidaClient: this.#aidaClient,
1289-
serverSideLoggingEnabled: this.#serverSideLoggingEnabled,
1290-
project: this.#project,
1291-
});
1292-
const responses = await Array.fromAsync(agent.applyChanges(changeSummary));
1293-
return responses.at(-1);
1294-
}
1295-
12961211
async *
12971212
#saveResponsesToCurrentConversation(items: AsyncIterable<ResponseData, void, void>):
12981213
AsyncGenerator<ResponseData, void, void> {
@@ -1487,11 +1402,6 @@ function isAiAssistanceMultimodalInputEnabled(): boolean {
14871402
return Boolean(hostConfig.devToolsFreestyler?.multimodal);
14881403
}
14891404

1490-
function isAiAssistancePatchingEnabled(): boolean {
1491-
const {hostConfig} = Root.Runtime;
1492-
return Boolean(hostConfig.devToolsFreestyler?.patching);
1493-
}
1494-
14951405
function isAiAssistanceServerSideLoggingEnabled(): boolean {
14961406
const {hostConfig} = Root.Runtime;
14971407
return !hostConfig.aidaAvailability?.disallowLogging;

front_end/panels/ai_assistance/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ devtools_module("ai_assistance") {
2323
"ChangeManager.ts",
2424
"EvaluateAction.ts",
2525
"ExtensionScope.ts",
26+
"PatchWidget.ts",
2627
"agents/AiAgent.ts",
2728
"agents/FileAgent.ts",
2829
"agents/NetworkAgent.ts",
@@ -99,6 +100,7 @@ ts_library("unittests") {
99100
"ChangeManager.test.ts",
100101
"EvaluateAction.test.ts",
101102
"ExtensionScope.test.ts",
103+
"PatchWidget.test.ts",
102104
"agents/AiAgent.test.ts",
103105
"agents/FileAgent.test.ts",
104106
"agents/NetworkAgent.test.ts",
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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 Platform from '../../core/platform/platform.js';
6+
import * as Workspace from '../../models/workspace/workspace.js';
7+
import {
8+
cleanup,
9+
createPatchWidget,
10+
} from '../../testing/AiAssistanceHelpers.js';
11+
import {updateHostConfig} from '../../testing/EnvironmentHelpers.js';
12+
import {describeWithMockConnection} from '../../testing/MockConnection.js';
13+
import {createFileSystemUISourceCode} from '../../testing/UISourceCodeHelpers.js';
14+
15+
describeWithMockConnection('workspace', () => {
16+
afterEach(() => {
17+
cleanup();
18+
});
19+
20+
function createTestFilesystem(fileSystemPath: string) {
21+
const {project, uiSourceCode} = createFileSystemUISourceCode({
22+
url: Platform.DevToolsPath.urlString`file:///example.html`,
23+
mimeType: 'text/html',
24+
content: 'content',
25+
fileSystemPath,
26+
});
27+
return {project, uiSourceCode};
28+
}
29+
30+
it('does not report a workspace project if disabled', async () => {
31+
createTestFilesystem('file://test');
32+
updateHostConfig({
33+
devToolsFreestyler: {
34+
enabled: true,
35+
patching: false,
36+
},
37+
});
38+
const {
39+
initialViewInput,
40+
} = await createPatchWidget();
41+
assert.isUndefined(initialViewInput.projectName);
42+
});
43+
44+
it('reports a current workspace project', async () => {
45+
createTestFilesystem('file://test');
46+
updateHostConfig({
47+
devToolsFreestyler: {
48+
enabled: true,
49+
patching: true,
50+
},
51+
});
52+
const {
53+
initialViewInput,
54+
} = await createPatchWidget();
55+
assert.strictEqual(initialViewInput.projectName, 'test');
56+
});
57+
58+
it('reports an updated project', async () => {
59+
const {project} = createTestFilesystem('file://test');
60+
updateHostConfig({
61+
devToolsFreestyler: {
62+
enabled: true,
63+
patching: true,
64+
},
65+
});
66+
const {initialViewInput, expectViewUpdate} = await createPatchWidget();
67+
assert.strictEqual(initialViewInput.projectName, 'test');
68+
69+
const updatedViewInput = await expectViewUpdate(() => {
70+
Workspace.Workspace.WorkspaceImpl.instance().removeProject(project);
71+
createTestFilesystem('file://test2');
72+
});
73+
assert.strictEqual(updatedViewInput.projectName, 'test2');
74+
});
75+
});

0 commit comments

Comments
 (0)