Skip to content

Commit acdf453

Browse files
OrKoNDevtools-frontend LUCI CQ
authored andcommitted
[AI Assistance] extract file operations
Bug: 399563024 Change-Id: Ib9b06888cea4a3d8290cf69ed64ef5ea1ad0ae16 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6321927 Commit-Queue: Alex Rudenko <[email protected]> Reviewed-by: Ergün Erdoğmuş <[email protected]>
1 parent dce9537 commit acdf453

File tree

6 files changed

+169
-63
lines changed

6 files changed

+169
-63
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,6 +1201,7 @@ grd_files_debug_sources = [
12011201
"front_end/panels/accessibility/accessibilityNode.css.js",
12021202
"front_end/panels/accessibility/accessibilityProperties.css.js",
12031203
"front_end/panels/accessibility/axBreadcrumbs.css.js",
1204+
"front_end/panels/ai_assistance/AgentProject.js",
12041205
"front_end/panels/ai_assistance/AiAssistancePanel.js",
12051206
"front_end/panels/ai_assistance/AiHistoryStorage.js",
12061207
"front_end/panels/ai_assistance/ChangeManager.js",
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2024 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 {describeWithEnvironment} from '../../testing/EnvironmentHelpers.js';
7+
import {createFileSystemUISourceCode} from '../../testing/UISourceCodeHelpers.js';
8+
9+
import * as AiAssistance from './ai_assistance.js';
10+
11+
describeWithEnvironment('AgentProject', () => {
12+
async function mockProject() {
13+
const {project, uiSourceCode} = createFileSystemUISourceCode({
14+
url: Platform.DevToolsPath.urlString`file:///path/to/overrides/example.html`,
15+
fileSystemPath: Platform.DevToolsPath.urlString`file:///path/to/overrides/`,
16+
mimeType: 'text/html',
17+
content: 'content',
18+
});
19+
20+
uiSourceCode.setWorkingCopy('content working copy');
21+
22+
return {project: new AiAssistance.AgentProject(project), uiSourceCode};
23+
}
24+
25+
it('can list files', async () => {
26+
const {project} = await mockProject();
27+
28+
assert.deepEqual(project.getFiles(), ['example.html']);
29+
});
30+
31+
it('can search files', async () => {
32+
const {project} = await mockProject();
33+
34+
assert.deepEqual(await project.searchFiles('content working copy'), [{
35+
columnNumber: 0,
36+
filepath: 'example.html',
37+
lineNumber: 0,
38+
matchLength: 20,
39+
}]);
40+
});
41+
42+
it('can read files', async () => {
43+
const {project} = await mockProject();
44+
45+
assert.deepEqual(project.readFile('example.html'), 'content working copy');
46+
});
47+
48+
it('can write files files', async () => {
49+
const {project} = await mockProject();
50+
project.writeFile('example.html', 'updated');
51+
assert.deepEqual(project.readFile('example.html'), 'updated');
52+
});
53+
});
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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 TextUtils from '../../models/text_utils/text_utils.js';
6+
import type * as Workspace from '../../models/workspace/workspace.js';
7+
8+
import {debugLog} from './debug.js';
9+
10+
/**
11+
* AgentProject wraps around a Workspace.Workspace.Project and
12+
* implements AI Assistance-specific logic for accessing workspace files
13+
* including additional checks and restrictions.
14+
*/
15+
export class AgentProject {
16+
#project: Workspace.Workspace.Project;
17+
18+
constructor(project: Workspace.Workspace.Project) {
19+
this.#project = project;
20+
}
21+
22+
#indexFiles(): {files: string[], map: Map<string, Workspace.UISourceCode.UISourceCode>} {
23+
const files = [];
24+
const map = new Map();
25+
for (const uiSourceCode of this.#project.uiSourceCodes()) {
26+
// fullDisplayName includes the project name. TODO: a better
27+
// getter for a relative file path is needed.
28+
let path = uiSourceCode.fullDisplayName();
29+
const idx = path.indexOf('/');
30+
if (idx !== -1) {
31+
path = path.substring(idx + 1);
32+
}
33+
files.push(path);
34+
map.set(path, uiSourceCode);
35+
}
36+
return {files, map};
37+
}
38+
39+
/**
40+
* Provides file names in the project to the agent.
41+
*/
42+
getFiles(): string[] {
43+
return this.#indexFiles().files;
44+
}
45+
46+
/**
47+
* Provides access to the file content in the working copy
48+
* of the matching UiSourceCode.
49+
*/
50+
readFile(filepath: string): string|undefined {
51+
const {map} = this.#indexFiles();
52+
const uiSourceCode = map.get(filepath);
53+
if (!uiSourceCode) {
54+
return;
55+
}
56+
// TODO: needs additional handling for binary files.
57+
return uiSourceCode.workingCopyContentData().text;
58+
}
59+
60+
/**
61+
* This method updates the file content in the working copy of the
62+
* UiSourceCode identified by the filepath.
63+
*/
64+
writeFile(filepath: string, content: string): void {
65+
const {map} = this.#indexFiles();
66+
const uiSourceCode = map.get(filepath);
67+
if (!uiSourceCode) {
68+
throw new Error(`UISourceCode ${filepath} not found`);
69+
}
70+
uiSourceCode.setWorkingCopy(content);
71+
}
72+
73+
/**
74+
* This method searches in files for the agent and provides the
75+
* matches to the agent.
76+
*/
77+
async searchFiles(query: string, caseSensitive?: boolean, isRegex?: boolean): Promise<Array<{
78+
filepath: string,
79+
lineNumber: number,
80+
columnNumber: number,
81+
matchLength: number,
82+
}>> {
83+
const {map} = this.#indexFiles();
84+
const matches = [];
85+
for (const [filepath, file] of map.entries()) {
86+
await file.requestContentData();
87+
debugLog('searching in', filepath, 'for', query);
88+
const content = file.isDirty() ? file.workingCopyContentData() : await file.requestContentData();
89+
const results =
90+
TextUtils.TextUtils.performSearchInContentData(content, query, caseSensitive ?? true, isRegex ?? false);
91+
for (const result of results) {
92+
debugLog('matches in', filepath);
93+
matches.push({
94+
filepath,
95+
lineNumber: result.lineNumber,
96+
columnNumber: result.columnNumber,
97+
matchLength: result.matchLength
98+
});
99+
}
100+
}
101+
return matches;
102+
}
103+
}

front_end/panels/ai_assistance/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ generate_css("css_files") {
1818

1919
devtools_module("ai_assistance") {
2020
sources = [
21+
"AgentProject.ts",
2122
"AiAssistancePanel.ts",
2223
"AiHistoryStorage.ts",
2324
"ChangeManager.ts",
@@ -96,6 +97,7 @@ ts_library("unittests") {
9697
testonly = true
9798

9899
sources = [
100+
"AgentProject.test.ts",
99101
"AiAssistancePanel.test.ts",
100102
"AiHistoryStorage.test.ts",
101103
"ChangeManager.test.ts",

front_end/panels/ai_assistance/agents/PatchAgent.ts

Lines changed: 9 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
// found in the LICENSE file.
44

55
import * as Host from '../../../core/host/host.js';
6-
import * as TextUtils from '../../../models/text_utils/text_utils.js';
76
import type * as Workspace from '../../../models/workspace/workspace.js';
7+
import {AgentProject} from '../AgentProject.js';
88
import {debugLog} from '../debug.js';
99

1010
import {
@@ -18,24 +18,8 @@ import {
1818
ResponseType,
1919
} from './AiAgent.js';
2020

21-
function getFiles(project: Workspace.Workspace.Project):
22-
{files: string[], map: Map<string, Workspace.UISourceCode.UISourceCode>} {
23-
const files = [];
24-
const map = new Map();
25-
for (const uiSourceCode of project.uiSourceCodes()) {
26-
let path = uiSourceCode.fullDisplayName();
27-
const idx = path.indexOf('/');
28-
if (idx !== -1) {
29-
path = path.substring(idx + 1);
30-
}
31-
files.push(path);
32-
map.set(path, uiSourceCode);
33-
}
34-
return {files, map};
35-
}
36-
3721
export class PatchAgent extends AiAgent<Workspace.Workspace.Project> {
38-
#project: Workspace.Workspace.Project;
22+
#project: AgentProject;
3923
#fileUpdateAgent: FileUpdateAgent;
4024
#changeSummary = '';
4125

@@ -63,7 +47,7 @@ export class PatchAgent extends AiAgent<Workspace.Workspace.Project> {
6347

6448
constructor(opts: BaseAgentOptions&{fileUpdateAgent?: FileUpdateAgent, project: Workspace.Workspace.Project}) {
6549
super(opts);
66-
this.#project = opts.project;
50+
this.#project = new AgentProject(opts.project);
6751
this.#fileUpdateAgent = opts.fileUpdateAgent ?? new FileUpdateAgent(opts);
6852
this.declareFunction<Record<never, unknown>>('listFiles', {
6953
description: 'Returns a list of all files in the project.',
@@ -74,16 +58,9 @@ export class PatchAgent extends AiAgent<Workspace.Workspace.Project> {
7458
properties: {},
7559
},
7660
handler: async () => {
77-
if (!this.#project) {
78-
return {
79-
error: 'No project available',
80-
};
81-
}
82-
const project = this.#project;
83-
const {files} = getFiles(project);
8461
return {
8562
result: {
86-
files,
63+
files: this.#project.getFiles(),
8764
}
8865
};
8966
},
@@ -119,33 +96,9 @@ export class PatchAgent extends AiAgent<Workspace.Workspace.Project> {
11996
},
12097
},
12198
handler: async params => {
122-
if (!this.#project) {
123-
return {
124-
error: 'No project available',
125-
};
126-
}
127-
const project = this.#project;
128-
const {map} = getFiles(project);
129-
const matches = [];
130-
for (const [filepath, file] of map.entries()) {
131-
await file.requestContentData();
132-
debugLog('searching in', filepath, 'for', params.query);
133-
const content = file.isDirty() ? file.workingCopyContentData() : await file.requestContentData();
134-
const results = TextUtils.TextUtils.performSearchInContentData(
135-
content, params.query, params.caseSensitive ?? true, params.isRegex ?? false);
136-
for (const result of results) {
137-
debugLog('matches in', filepath);
138-
matches.push({
139-
filepath,
140-
lineNumber: result.lineNumber,
141-
columnNumber: result.columnNumber,
142-
matchLength: result.matchLength
143-
});
144-
}
145-
}
14699
return {
147100
result: {
148-
matches,
101+
matches: await this.#project.searchFiles(params.query, params.caseSensitive, params.isRegex),
149102
}
150103
};
151104
},
@@ -170,17 +123,10 @@ export class PatchAgent extends AiAgent<Workspace.Workspace.Project> {
170123
},
171124
handler: async args => {
172125
debugLog('updateFiles', args.files);
173-
if (!this.#project) {
174-
return {
175-
error: 'No project available',
176-
};
177-
}
178-
const project = this.#project;
179-
const {map} = getFiles(project);
180126
for (const file of args.files.slice(0, 3)) {
181127
debugLog('updating', file);
182-
const uiSourceCode = map.get(file);
183-
if (!uiSourceCode) {
128+
const content = this.#project.readFile(file);
129+
if (content === undefined) {
184130
debugLog(file, 'not found');
185131
continue;
186132
}
@@ -194,7 +140,7 @@ Following '===' I provide the source code file. Update the file to apply the sam
194140
CRITICAL: Output the entire file with changes without any other modifications! DO NOT USE MARKDOWN.
195141
196142
===
197-
${uiSourceCode.workingCopyContentData().text}
143+
${content}
198144
`;
199145
let response;
200146
for await (response of this.#fileUpdateAgent.run(prompt, {selected: null})) {
@@ -206,7 +152,7 @@ ${uiSourceCode.workingCopyContentData().text}
206152
continue;
207153
}
208154
const updated = response.text;
209-
uiSourceCode.setWorkingCopy(updated);
155+
this.#project.writeFile(file, updated);
210156
debugLog('updated', updated);
211157
}
212158
return {

front_end/panels/ai_assistance/ai_assistance.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
export * from './debug.js';
6+
export * from './AgentProject.js';
67
export * from './agents/AiAgent.js';
78
export * from './agents/FileAgent.js';
89
export * from './agents/NetworkAgent.js';

0 commit comments

Comments
 (0)