Skip to content

Commit 57071ea

Browse files
committed
Integrate ChangeSet UI with Coder agent
Signed-off-by: Jonas Helming <[email protected]>
1 parent 2d6e9f9 commit 57071ea

10 files changed

+67
-669
lines changed

package-lock.json

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/ai-workspace-agent/src/browser/coder-agent.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { AgentSpecificVariables, PromptTemplate } from '@theia/ai-core';
1818
import { injectable } from '@theia/core/shared/inversify';
1919
import { FILE_CONTENT_FUNCTION_ID, GET_WORKSPACE_FILE_LIST_FUNCTION_ID, GET_WORKSPACE_DIRECTORY_STRUCTURE_FUNCTION_ID } from '../common/workspace-functions';
2020
import { coderReplacePromptTemplate } from '../common/coder-replace-prompt-template';
21+
import { WriteChangeToFileProvider } from './file-changeset-functions';
2122

2223
@injectable()
2324
export class CoderAgent extends AbstractStreamParsingChatAgent implements ChatAgent {
@@ -38,7 +39,7 @@ export class CoderAgent extends AbstractStreamParsingChatAgent implements ChatAg
3839
this.promptTemplates = [coderReplacePromptTemplate];
3940
this.variables = [];
4041
this.agentSpecificVariables = [];
41-
this.functions = [GET_WORKSPACE_DIRECTORY_STRUCTURE_FUNCTION_ID, GET_WORKSPACE_FILE_LIST_FUNCTION_ID, FILE_CONTENT_FUNCTION_ID];
42+
this.functions = [GET_WORKSPACE_DIRECTORY_STRUCTURE_FUNCTION_ID, GET_WORKSPACE_FILE_LIST_FUNCTION_ID, FILE_CONTENT_FUNCTION_ID, WriteChangeToFileProvider.ID];
4243
}
4344

4445
protected override async getSystemMessageDescription(): Promise<SystemMessageDescription | undefined> {

packages/ai-workspace-agent/src/browser/content-change-applier-service.ts

-103
This file was deleted.

packages/ai-workspace-agent/src/browser/file-changeset-functions.ts

+53-174
Original file line numberDiff line numberDiff line change
@@ -15,198 +15,77 @@
1515
// *****************************************************************************
1616
import { injectable, inject } from '@theia/core/shared/inversify';
1717
import { ToolProvider, ToolRequest } from '@theia/ai-core';
18-
import { FileChangeSetService } from './file-changeset-service';
18+
import { WorkspaceFunctionScope } from './workspace-functions';
19+
import { ChangeSetFileElementFactory } from '@theia/ai-chat/lib/browser/change-set-file-element';
20+
import { ChangeSetImpl, ChatRequestModelImpl } from '@theia/ai-chat';
21+
import { FileService } from '@theia/filesystem/lib/browser/file-service';
1922

2023
@injectable()
21-
export class InitializeChangeSetProvider implements ToolProvider {
22-
static ID = 'changeSet_initializeChangeSet';
24+
export class WriteChangeToFileProvider implements ToolProvider {
25+
static ID = 'changeSet_writeChangeToFile';
2326

24-
@inject(FileChangeSetService)
25-
protected readonly changeSetService: FileChangeSetService;
27+
@inject(WorkspaceFunctionScope)
28+
protected readonly workspaceFunctionScope: WorkspaceFunctionScope;
2629

27-
getTool(): ToolRequest {
28-
return {
29-
id: InitializeChangeSetProvider.ID,
30-
name: InitializeChangeSetProvider.ID,
31-
description: 'Creates a new change set with a unique UUID and description.',
32-
parameters: {
33-
type: 'object',
34-
properties: {
35-
uuid: { type: 'string', description: 'Unique identifier for the change set.' },
36-
description: { type: 'string', description: 'High-level description of the change set.' }
37-
},
38-
required: ['uuid', 'description']
39-
},
40-
handler: async (args: string): Promise<string> => {
41-
try {
42-
const { uuid, description } = JSON.parse(args);
43-
this.changeSetService.initializeChangeSet(uuid, description);
44-
return `Change set ${uuid} initialized successfully.`;
45-
} catch (error) {
46-
return JSON.stringify({ error: error.message });
47-
}
48-
}
49-
};
50-
}
51-
}
52-
53-
@injectable()
54-
export class RemoveFileChangeProvider implements ToolProvider {
55-
static ID = 'changeSet_removeFileChange';
56-
57-
@inject(FileChangeSetService)
58-
protected readonly changeSetService: FileChangeSetService;
59-
60-
getTool(): ToolRequest {
61-
return {
62-
id: RemoveFileChangeProvider.ID,
63-
name: RemoveFileChangeProvider.ID,
64-
description: 'Removes a file and all related changes from the specified change set.',
65-
parameters: {
66-
type: 'object',
67-
properties: {
68-
uuid: { type: 'string', description: 'Unique identifier for the change set.' },
69-
filePath: { type: 'string', description: 'Path to the file.' }
70-
},
71-
required: ['uuid', 'filePath']
72-
},
73-
handler: async (args: string): Promise<string> => {
74-
try {
75-
const { uuid, filePath } = JSON.parse(args);
76-
this.changeSetService.removeFileChange(uuid, filePath);
77-
return `File ${filePath} removed from change set ${uuid}.`;
78-
} catch (error) {
79-
return JSON.stringify({ error: error.message });
80-
}
81-
}
82-
};
83-
}
84-
}
85-
86-
@injectable()
87-
export class ListChangedFilesProvider implements ToolProvider {
88-
static ID = 'changeSet_listChangedFiles';
89-
90-
@inject(FileChangeSetService)
91-
protected readonly changeSetService: FileChangeSetService;
92-
93-
getTool(): ToolRequest {
94-
return {
95-
id: ListChangedFilesProvider.ID,
96-
name: ListChangedFilesProvider.ID,
97-
description: 'Lists all files included in a specific change set.',
98-
parameters: {
99-
type: 'object',
100-
properties: {
101-
uuid: { type: 'string', description: 'Unique identifier for the change set.' }
102-
},
103-
required: ['uuid']
104-
},
105-
handler: async (args: string): Promise<string> => {
106-
try {
107-
const { uuid } = JSON.parse(args);
108-
const files = this.changeSetService.listChangedFiles(uuid);
109-
return JSON.stringify(files);
110-
} catch (error) {
111-
return JSON.stringify({ error: error.message });
112-
}
113-
}
114-
};
115-
}
116-
}
117-
118-
@injectable()
119-
export class GetFileChangesProvider implements ToolProvider {
120-
static ID = 'changeSet_getFileChanges';
30+
@inject(FileService)
31+
fileService: FileService;
12132

122-
@inject(FileChangeSetService)
123-
protected readonly changeSetService: FileChangeSetService;
33+
@inject(ChangeSetFileElementFactory)
34+
protected readonly fileChangeFactory: ChangeSetFileElementFactory;
12435

12536
getTool(): ToolRequest {
12637
return {
127-
id: GetFileChangesProvider.ID,
128-
name: GetFileChangesProvider.ID,
129-
description: 'Fetches the operations of a specific file in a change set.',
38+
id: WriteChangeToFileProvider.ID,
39+
name: WriteChangeToFileProvider.ID,
40+
description: `Proposes writing content to a file. If the file exists, it will be overwritten with the provided content.\n
41+
If the file does not exist, it will be created. This tool will automatically create any directories needed to write the file.\n
42+
If the new content is empty, the file will be deleted. To move a file, delete it and re-create it at the new location.\n
43+
The proposed changes will be applied when the user accepts.`,
13044
parameters: {
13145
type: 'object',
13246
properties: {
133-
uuid: { type: 'string', description: 'Unique identifier for the change set.' },
134-
filePath: { type: 'string', description: 'Path to the file.' }
47+
path: {
48+
type: 'string',
49+
description: 'The path of the file to write to.'
50+
},
51+
content: {
52+
type: 'string',
53+
description: `The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions.\n
54+
You MUST include ALL parts of the file, even if they haven\'t been modified.`
55+
}
13556
},
136-
required: ['uuid', 'filePath']
57+
required: ['path', 'content']
13758
},
138-
handler: async (args: string): Promise<string> => {
139-
try {
140-
const { uuid, filePath } = JSON.parse(args);
141-
const changes = this.changeSetService.getFileChanges(uuid, filePath);
142-
return JSON.stringify(changes);
143-
} catch (error) {
144-
return JSON.stringify({ error: error.message });
59+
handler: async (args: string, ctx: ChatRequestModelImpl): Promise<string> => {
60+
const { path, content } = JSON.parse(args);
61+
const chatSessionId = ctx.session.id;
62+
let changeSet = ctx.session.changeSet;
63+
if (!changeSet) {
64+
changeSet = new ChangeSetImpl('Changes proposed by Coder');
65+
ctx.session.setChangeSet(changeSet);
14566
}
146-
}
147-
};
148-
}
149-
}
150-
151-
@injectable()
152-
export class GetChangeSetProvider implements ToolProvider {
153-
static ID = 'changeSet_getChangeSet';
154-
155-
@inject(FileChangeSetService)
156-
protected readonly changeSetService: FileChangeSetService;
157-
158-
getTool(): ToolRequest {
159-
return {
160-
id: GetChangeSetProvider.ID,
161-
name: GetChangeSetProvider.ID,
162-
description: 'Fetches the details of a specific change set.',
163-
parameters: {
164-
type: 'object',
165-
properties: {
166-
uuid: { type: 'string', description: 'Unique identifier for the change set.' }
167-
},
168-
required: ['uuid']
169-
},
170-
handler: async (args: string): Promise<string> => {
171-
try {
172-
const { uuid } = JSON.parse(args);
173-
const changeSet = this.changeSetService.getChangeSet(uuid);
174-
return JSON.stringify(changeSet);
175-
} catch (error) {
176-
return JSON.stringify({ error: error.message });
67+
const uri = await this.workspaceFunctionScope.resolveRelativePath(path);
68+
let type = 'modify';
69+
if (content === '') {
70+
type = 'delete';
17771
}
178-
}
179-
};
180-
}
181-
}
182-
183-
@injectable()
184-
export class ApplyChangeSetProvider implements ToolProvider {
185-
static ID = 'changeSet_applyChangeSet';
186-
187-
@inject(FileChangeSetService)
188-
protected readonly changeSetService: FileChangeSetService;
189-
190-
getTool(): ToolRequest {
191-
return {
192-
id: ApplyChangeSetProvider.ID,
193-
name: ApplyChangeSetProvider.ID,
194-
description: 'Applies the specified change set by UUID, executing all file modifications described within.',
195-
parameters: {
196-
type: 'object',
197-
properties: {
198-
uuid: { type: 'string', description: 'Unique identifier for the change set to apply.' }
199-
},
200-
required: ['uuid']
201-
},
202-
handler: async (args: string): Promise<string> => {
72+
// In case the file does not exist and the content is empty, we consider that the AI wants to add an empty file.
20373
try {
204-
const { uuid } = JSON.parse(args);
205-
await this.changeSetService.applyChangeSet(uuid);
206-
return `Change set ${uuid} applied successfully.`;
74+
await this.fileService.read(uri);
20775
} catch (error) {
208-
return JSON.stringify({ error: error.message });
76+
type = 'add';
20977
}
78+
changeSet.addElement(
79+
this.fileChangeFactory({
80+
uri: uri,
81+
type: type as 'modify' | 'add' | 'delete',
82+
state: 'pending',
83+
targetState: content,
84+
changeSet,
85+
chatSessionId
86+
})
87+
);
88+
return `Proposed writing to file ${path}. The user will review and potentially apply the changes`;
21089
}
21190
};
21291
}

0 commit comments

Comments
 (0)