@theia/ai-claude-code 1.65.0
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.
- package/README.md +30 -0
- package/lib/browser/claude-code-chat-agent.d.ts +82 -0
- package/lib/browser/claude-code-chat-agent.d.ts.map +1 -0
- package/lib/browser/claude-code-chat-agent.js +518 -0
- package/lib/browser/claude-code-chat-agent.js.map +1 -0
- package/lib/browser/claude-code-command-contribution.d.ts +16 -0
- package/lib/browser/claude-code-command-contribution.d.ts.map +1 -0
- package/lib/browser/claude-code-command-contribution.js +86 -0
- package/lib/browser/claude-code-command-contribution.js.map +1 -0
- package/lib/browser/claude-code-edit-tool-service.d.ts +61 -0
- package/lib/browser/claude-code-edit-tool-service.d.ts.map +1 -0
- package/lib/browser/claude-code-edit-tool-service.js +219 -0
- package/lib/browser/claude-code-edit-tool-service.js.map +1 -0
- package/lib/browser/claude-code-file-edit-backup-service.d.ts +57 -0
- package/lib/browser/claude-code-file-edit-backup-service.d.ts.map +1 -0
- package/lib/browser/claude-code-file-edit-backup-service.js +92 -0
- package/lib/browser/claude-code-file-edit-backup-service.js.map +1 -0
- package/lib/browser/claude-code-frontend-module.d.ts +5 -0
- package/lib/browser/claude-code-frontend-module.d.ts.map +1 -0
- package/lib/browser/claude-code-frontend-module.js +83 -0
- package/lib/browser/claude-code-frontend-module.js.map +1 -0
- package/lib/browser/claude-code-frontend-service.d.ts +40 -0
- package/lib/browser/claude-code-frontend-service.d.ts.map +1 -0
- package/lib/browser/claude-code-frontend-service.js +190 -0
- package/lib/browser/claude-code-frontend-service.js.map +1 -0
- package/lib/browser/claude-code-slash-commands-contribution.d.ts +17 -0
- package/lib/browser/claude-code-slash-commands-contribution.d.ts.map +1 -0
- package/lib/browser/claude-code-slash-commands-contribution.js +154 -0
- package/lib/browser/claude-code-slash-commands-contribution.js.map +1 -0
- package/lib/browser/claude-code-tool-call-content.d.ts +8 -0
- package/lib/browser/claude-code-tool-call-content.d.ts.map +1 -0
- package/lib/browser/claude-code-tool-call-content.js +30 -0
- package/lib/browser/claude-code-tool-call-content.js.map +1 -0
- package/lib/browser/renderers/bash-tool-renderer.d.ts +10 -0
- package/lib/browser/renderers/bash-tool-renderer.d.ts.map +1 -0
- package/lib/browser/renderers/bash-tool-renderer.js +71 -0
- package/lib/browser/renderers/bash-tool-renderer.js.map +1 -0
- package/lib/browser/renderers/collapsible-tool-renderer.d.ts +13 -0
- package/lib/browser/renderers/collapsible-tool-renderer.d.ts.map +1 -0
- package/lib/browser/renderers/collapsible-tool-renderer.js +48 -0
- package/lib/browser/renderers/collapsible-tool-renderer.js.map +1 -0
- package/lib/browser/renderers/edit-tool-renderer.d.ts +16 -0
- package/lib/browser/renderers/edit-tool-renderer.d.ts.map +1 -0
- package/lib/browser/renderers/edit-tool-renderer.js +134 -0
- package/lib/browser/renderers/edit-tool-renderer.js.map +1 -0
- package/lib/browser/renderers/glob-tool-renderer.d.ts +14 -0
- package/lib/browser/renderers/glob-tool-renderer.d.ts.map +1 -0
- package/lib/browser/renderers/glob-tool-renderer.js +107 -0
- package/lib/browser/renderers/glob-tool-renderer.js.map +1 -0
- package/lib/browser/renderers/grep-tool-renderer.d.ts +14 -0
- package/lib/browser/renderers/grep-tool-renderer.d.ts.map +1 -0
- package/lib/browser/renderers/grep-tool-renderer.js +157 -0
- package/lib/browser/renderers/grep-tool-renderer.js.map +1 -0
- package/lib/browser/renderers/ls-tool-renderer.d.ts +16 -0
- package/lib/browser/renderers/ls-tool-renderer.d.ts.map +1 -0
- package/lib/browser/renderers/ls-tool-renderer.js +116 -0
- package/lib/browser/renderers/ls-tool-renderer.js.map +1 -0
- package/lib/browser/renderers/multiedit-tool-renderer.d.ts +16 -0
- package/lib/browser/renderers/multiedit-tool-renderer.d.ts.map +1 -0
- package/lib/browser/renderers/multiedit-tool-renderer.js +152 -0
- package/lib/browser/renderers/multiedit-tool-renderer.js.map +1 -0
- package/lib/browser/renderers/read-tool-renderer.d.ts +16 -0
- package/lib/browser/renderers/read-tool-renderer.d.ts.map +1 -0
- package/lib/browser/renderers/read-tool-renderer.js +121 -0
- package/lib/browser/renderers/read-tool-renderer.js.map +1 -0
- package/lib/browser/renderers/todo-write-renderer.d.ts +10 -0
- package/lib/browser/renderers/todo-write-renderer.d.ts.map +1 -0
- package/lib/browser/renderers/todo-write-renderer.js +132 -0
- package/lib/browser/renderers/todo-write-renderer.js.map +1 -0
- package/lib/browser/renderers/web-fetch-tool-renderer.d.ts +10 -0
- package/lib/browser/renderers/web-fetch-tool-renderer.d.ts.map +1 -0
- package/lib/browser/renderers/web-fetch-tool-renderer.js +82 -0
- package/lib/browser/renderers/web-fetch-tool-renderer.js.map +1 -0
- package/lib/browser/renderers/write-tool-renderer.d.ts +16 -0
- package/lib/browser/renderers/write-tool-renderer.d.ts.map +1 -0
- package/lib/browser/renderers/write-tool-renderer.js +113 -0
- package/lib/browser/renderers/write-tool-renderer.js.map +1 -0
- package/lib/common/claude-code-preferences.d.ts +4 -0
- package/lib/common/claude-code-preferences.d.ts.map +1 -0
- package/lib/common/claude-code-preferences.js +33 -0
- package/lib/common/claude-code-preferences.js.map +1 -0
- package/lib/common/claude-code-service.d.ts +231 -0
- package/lib/common/claude-code-service.d.ts.map +1 -0
- package/lib/common/claude-code-service.js +82 -0
- package/lib/common/claude-code-service.js.map +1 -0
- package/lib/common/index.d.ts +2 -0
- package/lib/common/index.d.ts.map +1 -0
- package/lib/common/index.js +20 -0
- package/lib/common/index.js.map +1 -0
- package/lib/node/claude-code-backend-module.d.ts +4 -0
- package/lib/node/claude-code-backend-module.d.ts.map +1 -0
- package/lib/node/claude-code-backend-module.js +35 -0
- package/lib/node/claude-code-backend-module.js.map +1 -0
- package/lib/node/claude-code-service-impl.d.ts +32 -0
- package/lib/node/claude-code-service-impl.d.ts.map +1 -0
- package/lib/node/claude-code-service-impl.js +426 -0
- package/lib/node/claude-code-service-impl.js.map +1 -0
- package/lib/package.spec.d.ts +1 -0
- package/lib/package.spec.d.ts.map +1 -0
- package/lib/package.spec.js +26 -0
- package/lib/package.spec.js.map +1 -0
- package/package.json +57 -0
- package/src/browser/claude-code-chat-agent.ts +591 -0
- package/src/browser/claude-code-command-contribution.ts +80 -0
- package/src/browser/claude-code-edit-tool-service.ts +313 -0
- package/src/browser/claude-code-file-edit-backup-service.ts +141 -0
- package/src/browser/claude-code-frontend-module.ts +100 -0
- package/src/browser/claude-code-frontend-service.ts +215 -0
- package/src/browser/claude-code-slash-commands-contribution.ts +175 -0
- package/src/browser/claude-code-tool-call-content.ts +30 -0
- package/src/browser/renderers/bash-tool-renderer.tsx +97 -0
- package/src/browser/renderers/collapsible-tool-renderer.tsx +78 -0
- package/src/browser/renderers/edit-tool-renderer.tsx +180 -0
- package/src/browser/renderers/glob-tool-renderer.tsx +136 -0
- package/src/browser/renderers/grep-tool-renderer.tsx +190 -0
- package/src/browser/renderers/ls-tool-renderer.tsx +160 -0
- package/src/browser/renderers/multiedit-tool-renderer.tsx +204 -0
- package/src/browser/renderers/read-tool-renderer.tsx +170 -0
- package/src/browser/renderers/todo-write-renderer.tsx +178 -0
- package/src/browser/renderers/web-fetch-tool-renderer.tsx +108 -0
- package/src/browser/renderers/write-tool-renderer.tsx +155 -0
- package/src/browser/style/claude-code-tool-renderers.css +487 -0
- package/src/common/claude-code-preferences.ts +33 -0
- package/src/common/claude-code-service.ts +303 -0
- package/src/common/index.ts +17 -0
- package/src/node/claude-code-backend-module.ts +42 -0
- package/src/node/claude-code-service-impl.ts +462 -0
- package/src/package.spec.ts +27 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { MutableChatRequestModel } from '@theia/ai-chat';
|
|
18
|
+
import { ChangeSetFileElement, ChangeSetFileElementFactory } from '@theia/ai-chat/lib/browser/change-set-file-element';
|
|
19
|
+
import { ChangeSetElement } from '@theia/ai-chat/lib/common/change-set';
|
|
20
|
+
import { ContentReplacer, Replacement } from '@theia/core/lib/common/content-replacer';
|
|
21
|
+
import { URI } from '@theia/core/lib/common/uri';
|
|
22
|
+
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
23
|
+
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
24
|
+
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
|
25
|
+
import { FileEditBackupService } from './claude-code-file-edit-backup-service';
|
|
26
|
+
|
|
27
|
+
export interface EditToolInput {
|
|
28
|
+
file_path: string;
|
|
29
|
+
old_string: string;
|
|
30
|
+
new_string: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface MultiEditToolInput {
|
|
34
|
+
file_path: string;
|
|
35
|
+
edits: Array<{
|
|
36
|
+
old_string: string;
|
|
37
|
+
new_string: string;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface WriteToolInput {
|
|
42
|
+
file_path: string;
|
|
43
|
+
content: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ToolUseBlock {
|
|
47
|
+
name: string;
|
|
48
|
+
input: EditToolInput | MultiEditToolInput | WriteToolInput;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface EditToolContext {
|
|
52
|
+
sessionId: string | undefined;
|
|
53
|
+
isEditMode: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const ClaudeCodeEditToolService = Symbol('ClaudeCodeEditToolService');
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Service for handling edit tool operations.
|
|
60
|
+
*
|
|
61
|
+
* Invoked by the ClaudeCodeChatAgent on each finished edit tool request.
|
|
62
|
+
* This can be used to track and manage file edits made by the agent, e.g.
|
|
63
|
+
* to propagate them to ChangeSets (see ClaudeCodeEditToolServiceImpl below).
|
|
64
|
+
*/
|
|
65
|
+
export interface ClaudeCodeEditToolService {
|
|
66
|
+
handleEditTool(toolUse: ToolUseBlock, request: MutableChatRequestModel, context: EditToolContext): Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Propagates edit tool results to change sets in the specified request's session.
|
|
71
|
+
*/
|
|
72
|
+
@injectable()
|
|
73
|
+
export class ClaudeCodeEditToolServiceImpl implements ClaudeCodeEditToolService {
|
|
74
|
+
|
|
75
|
+
@inject(ChangeSetFileElementFactory)
|
|
76
|
+
protected readonly fileChangeFactory: ChangeSetFileElementFactory;
|
|
77
|
+
|
|
78
|
+
@inject(FileService)
|
|
79
|
+
protected readonly fileService: FileService;
|
|
80
|
+
|
|
81
|
+
@inject(WorkspaceService)
|
|
82
|
+
protected readonly workspaceService: WorkspaceService;
|
|
83
|
+
|
|
84
|
+
@inject(FileEditBackupService)
|
|
85
|
+
protected readonly backupService: FileEditBackupService;
|
|
86
|
+
|
|
87
|
+
private readonly contentReplacer = new ContentReplacer();
|
|
88
|
+
|
|
89
|
+
async handleEditTool(toolUse: ToolUseBlock, request: MutableChatRequestModel, context: EditToolContext): Promise<void> {
|
|
90
|
+
try {
|
|
91
|
+
const { name, input } = toolUse;
|
|
92
|
+
|
|
93
|
+
switch (name) {
|
|
94
|
+
case 'Edit':
|
|
95
|
+
await this.handleEditSingle(input as EditToolInput, request, context);
|
|
96
|
+
break;
|
|
97
|
+
case 'MultiEdit':
|
|
98
|
+
await this.handleEditMultiple(input as MultiEditToolInput, request, context);
|
|
99
|
+
break;
|
|
100
|
+
case 'Write':
|
|
101
|
+
await this.handleWriteFile(input as WriteToolInput, request, context);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('Error handling edit tool:', error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
protected async handleEditSingle(input: EditToolInput, request: MutableChatRequestModel, context: EditToolContext): Promise<void> {
|
|
110
|
+
try {
|
|
111
|
+
const workspaceUri = await this.toWorkspaceUri(input.file_path);
|
|
112
|
+
const currentContent = await this.fileService.read(workspaceUri);
|
|
113
|
+
const currentContentString = currentContent.value.toString();
|
|
114
|
+
const existingChangeSetElement = request.session.changeSet.getElementByURI(workspaceUri);
|
|
115
|
+
|
|
116
|
+
const replacement: Replacement = {
|
|
117
|
+
oldContent: input.old_string,
|
|
118
|
+
newContent: input.new_string
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (context.isEditMode) {
|
|
122
|
+
await this.handleEditModeCommon(
|
|
123
|
+
workspaceUri,
|
|
124
|
+
currentContentString,
|
|
125
|
+
[replacement],
|
|
126
|
+
existingChangeSetElement,
|
|
127
|
+
request,
|
|
128
|
+
context
|
|
129
|
+
);
|
|
130
|
+
} else {
|
|
131
|
+
await this.handleNonEditModeCommon(
|
|
132
|
+
workspaceUri,
|
|
133
|
+
currentContentString,
|
|
134
|
+
[replacement],
|
|
135
|
+
existingChangeSetElement,
|
|
136
|
+
request
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
request.session.changeSet.setTitle('Changes by Claude Code');
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('Error handling Edit tool:', error);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
protected async handleEditMultiple(input: MultiEditToolInput, request: MutableChatRequestModel, context: EditToolContext): Promise<void> {
|
|
147
|
+
try {
|
|
148
|
+
const workspaceUri = await this.toWorkspaceUri(input.file_path);
|
|
149
|
+
const currentContent = await this.fileService.read(workspaceUri);
|
|
150
|
+
const currentContentString = currentContent.value.toString();
|
|
151
|
+
const existingChangeSetElement = request.session.changeSet.getElementByURI(workspaceUri);
|
|
152
|
+
|
|
153
|
+
const replacements: Replacement[] = input.edits.map(edit => ({
|
|
154
|
+
oldContent: edit.old_string,
|
|
155
|
+
newContent: edit.new_string
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
if (context.isEditMode) {
|
|
159
|
+
await this.handleEditModeCommon(
|
|
160
|
+
workspaceUri,
|
|
161
|
+
currentContentString,
|
|
162
|
+
replacements,
|
|
163
|
+
existingChangeSetElement,
|
|
164
|
+
request,
|
|
165
|
+
context
|
|
166
|
+
);
|
|
167
|
+
} else {
|
|
168
|
+
await this.handleNonEditModeCommon(
|
|
169
|
+
workspaceUri,
|
|
170
|
+
currentContentString,
|
|
171
|
+
replacements,
|
|
172
|
+
existingChangeSetElement,
|
|
173
|
+
request
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
request.session.changeSet.setTitle('Changes by Claude Code');
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error('Error handling MultiEdit tool:', error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
protected async handleWriteFile(input: WriteToolInput, request: MutableChatRequestModel, context: EditToolContext): Promise<void> {
|
|
184
|
+
try {
|
|
185
|
+
const workspaceUri = await this.toWorkspaceUri(input.file_path);
|
|
186
|
+
const fileExists = await this.fileService.exists(workspaceUri);
|
|
187
|
+
|
|
188
|
+
if (context.isEditMode) {
|
|
189
|
+
if (input.content === '') {
|
|
190
|
+
const originalState = await this.backupService.getOriginal(workspaceUri, context.sessionId);
|
|
191
|
+
const fileElement = this.fileChangeFactory({
|
|
192
|
+
uri: workspaceUri,
|
|
193
|
+
type: 'delete',
|
|
194
|
+
state: 'applied',
|
|
195
|
+
originalState,
|
|
196
|
+
targetState: '',
|
|
197
|
+
requestId: request.id,
|
|
198
|
+
chatSessionId: request.session.id
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
request.session.changeSet.addElements(fileElement);
|
|
202
|
+
} else {
|
|
203
|
+
const type = !fileExists ? 'add' : 'modify';
|
|
204
|
+
let originalState = '';
|
|
205
|
+
if (type === 'modify') {
|
|
206
|
+
originalState = (await this.backupService.getOriginal(workspaceUri, context.sessionId)) ?? '';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const fileElement = this.fileChangeFactory({
|
|
210
|
+
uri: workspaceUri,
|
|
211
|
+
type,
|
|
212
|
+
state: 'applied',
|
|
213
|
+
originalState,
|
|
214
|
+
targetState: input.content,
|
|
215
|
+
requestId: request.id,
|
|
216
|
+
chatSessionId: request.session.id
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
request.session.changeSet.addElements(fileElement);
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
const type = input.content === '' ? 'delete' :
|
|
223
|
+
!fileExists ? 'add' : 'modify';
|
|
224
|
+
|
|
225
|
+
const fileElement = this.fileChangeFactory({
|
|
226
|
+
uri: workspaceUri,
|
|
227
|
+
type,
|
|
228
|
+
state: 'pending',
|
|
229
|
+
targetState: input.content,
|
|
230
|
+
requestId: request.id,
|
|
231
|
+
chatSessionId: request.session.id
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
request.session.changeSet.addElements(fileElement);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
request.session.changeSet.setTitle('Changes by Claude Code');
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.error('Error handling Write tool:', error);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
protected async handleEditModeCommon(
|
|
244
|
+
workspaceUri: URI,
|
|
245
|
+
currentContentString: string,
|
|
246
|
+
replacements: Replacement[],
|
|
247
|
+
existingChangeSetElement: ChangeSetElement | undefined,
|
|
248
|
+
request: MutableChatRequestModel,
|
|
249
|
+
context: EditToolContext
|
|
250
|
+
): Promise<void> {
|
|
251
|
+
const originalState = await this.backupService.getOriginal(workspaceUri, context.sessionId);
|
|
252
|
+
const existingReplacements = (existingChangeSetElement instanceof ChangeSetFileElement) && existingChangeSetElement.replacements || [];
|
|
253
|
+
|
|
254
|
+
const fileElement = this.fileChangeFactory({
|
|
255
|
+
uri: workspaceUri,
|
|
256
|
+
type: 'modify',
|
|
257
|
+
state: 'applied',
|
|
258
|
+
originalState,
|
|
259
|
+
targetState: currentContentString,
|
|
260
|
+
requestId: request.id,
|
|
261
|
+
chatSessionId: request.session.id,
|
|
262
|
+
replacements: [...existingReplacements, ...replacements]
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
request.session.changeSet.addElements(fileElement);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
protected async handleNonEditModeCommon(
|
|
269
|
+
workspaceUri: URI,
|
|
270
|
+
currentContentString: string,
|
|
271
|
+
replacements: Replacement[],
|
|
272
|
+
existingChangeSetElement: ChangeSetElement | undefined,
|
|
273
|
+
request: MutableChatRequestModel
|
|
274
|
+
): Promise<void> {
|
|
275
|
+
const { updatedContent, errors } = this.contentReplacer.applyReplacements(
|
|
276
|
+
currentContentString,
|
|
277
|
+
replacements
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
if (errors.length > 0) {
|
|
281
|
+
console.error('Content replacement errors:', errors);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (updatedContent !== currentContentString) {
|
|
286
|
+
const existingReplacements = (existingChangeSetElement instanceof ChangeSetFileElement) && existingChangeSetElement.replacements || [];
|
|
287
|
+
|
|
288
|
+
const fileElement = this.fileChangeFactory({
|
|
289
|
+
uri: workspaceUri,
|
|
290
|
+
type: 'modify',
|
|
291
|
+
state: 'pending',
|
|
292
|
+
targetState: updatedContent,
|
|
293
|
+
requestId: request.id,
|
|
294
|
+
chatSessionId: request.session.id,
|
|
295
|
+
replacements: [...existingReplacements, ...replacements]
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
request.session.changeSet.addElements(fileElement);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
protected async toWorkspaceUri(absolutePath: string): Promise<URI> {
|
|
303
|
+
const absoluteUri = new URI(absolutePath);
|
|
304
|
+
const workspaceUri = this.workspaceService.getWorkspaceRootUri(absoluteUri);
|
|
305
|
+
if (!workspaceUri) {
|
|
306
|
+
throw new Error(`No workspace found for ${absolutePath}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const relativeUri = await this.workspaceService.getWorkspaceRelativePath(absoluteUri);
|
|
310
|
+
return workspaceUri?.resolve(relativeUri);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { MutableChatRequestModel } from '@theia/ai-chat';
|
|
18
|
+
import { ChangeSetFileElement } from '@theia/ai-chat/lib/browser/change-set-file-element';
|
|
19
|
+
import { URI } from '@theia/core/lib/common/uri';
|
|
20
|
+
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
21
|
+
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
22
|
+
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
|
23
|
+
import { CLAUDE_SESSION_ID_KEY } from './claude-code-chat-agent';
|
|
24
|
+
|
|
25
|
+
export const FileEditBackupService = Symbol('FileEditBackupService');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Service for managing file backup operations during Claude Code edit sessions.
|
|
29
|
+
*
|
|
30
|
+
* This service handles the retrieval of original file content from backup files
|
|
31
|
+
* created by the file backup hooks in ClaudeCodeServiceImpl. The backup hooks
|
|
32
|
+
* run before file modification tools (Write, Edit, MultiEdit) and create backups
|
|
33
|
+
* in the `.claude/.edit-baks/{session_id}/` directory structure.
|
|
34
|
+
*
|
|
35
|
+
* @see packages/ai-claude-code/src/node/claude-code-service-impl.ts#ensureFileBackupHook
|
|
36
|
+
* The coupling with the backup hooks is intentional - this service reads from
|
|
37
|
+
* the same backup location that the hooks write to.
|
|
38
|
+
*/
|
|
39
|
+
export interface FileEditBackupService {
|
|
40
|
+
/**
|
|
41
|
+
* Retrieves the original content of a file from its backup.
|
|
42
|
+
*
|
|
43
|
+
* This method reads from backup files created by the file backup hooks
|
|
44
|
+
* that are installed by ClaudeCodeServiceImpl.ensureFileBackupHook().
|
|
45
|
+
*
|
|
46
|
+
* @param workspaceUri The URI of the file to get backup content for
|
|
47
|
+
* @param sessionId The Claude session ID used for backup organization
|
|
48
|
+
* @returns The original file content, or undefined if no backup exists
|
|
49
|
+
*/
|
|
50
|
+
getOriginal(workspaceUri: URI, sessionId: string | undefined): Promise<string | undefined>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Cleans up backup files for a completed chat session.
|
|
54
|
+
*
|
|
55
|
+
* This method removes the backup directory structure for the given session
|
|
56
|
+
* from all workspaces that have change set elements.
|
|
57
|
+
*
|
|
58
|
+
* @param request The chat request model containing session and change set information
|
|
59
|
+
*/
|
|
60
|
+
cleanUp(request: MutableChatRequestModel): Promise<void>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Gets the backup location for a workspace root.
|
|
64
|
+
*
|
|
65
|
+
* This must match the backup location used by the file backup hooks
|
|
66
|
+
* in ClaudeCodeServiceImpl.
|
|
67
|
+
*
|
|
68
|
+
* @param workspaceRoot The workspace root URI
|
|
69
|
+
* @returns The backup directory URI
|
|
70
|
+
*/
|
|
71
|
+
getLocation(workspaceRoot: URI): URI;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@injectable()
|
|
75
|
+
export class FileEditBackupServiceImpl implements FileEditBackupService {
|
|
76
|
+
|
|
77
|
+
@inject(FileService)
|
|
78
|
+
protected readonly fileService: FileService;
|
|
79
|
+
|
|
80
|
+
@inject(WorkspaceService)
|
|
81
|
+
protected readonly workspaceService: WorkspaceService;
|
|
82
|
+
|
|
83
|
+
getLocation(workspaceRoot: URI): URI {
|
|
84
|
+
// This path structure must match the backup hooks in claude-code-service-impl.ts
|
|
85
|
+
// See ensureFileBackupHook() method which creates backups at:
|
|
86
|
+
// path.join(hookData.cwd, '.claude', '.edit-baks', hookData.session_id)
|
|
87
|
+
return workspaceRoot.resolve('.claude').resolve('.edit-baks');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async getOriginal(workspaceUri: URI, sessionId: string | undefined): Promise<string | undefined> {
|
|
91
|
+
if (!sessionId) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const workspaceRoot = this.workspaceService.getWorkspaceRootUri(workspaceUri);
|
|
97
|
+
if (!workspaceRoot) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const relativePath = await this.workspaceService.getWorkspaceRelativePath(workspaceUri);
|
|
102
|
+
// This path structure must match the backup hooks in claude-code-service-impl.ts
|
|
103
|
+
const backupPath = this.getLocation(workspaceRoot).resolve(sessionId).resolve(relativePath);
|
|
104
|
+
|
|
105
|
+
if (await this.fileService.exists(backupPath)) {
|
|
106
|
+
const backupContent = await this.fileService.read(backupPath);
|
|
107
|
+
return backupContent.value.toString();
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('Error reading backup file:', error);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async cleanUp(request: MutableChatRequestModel): Promise<void> {
|
|
117
|
+
const sessionId = request.getDataByKey(CLAUDE_SESSION_ID_KEY) as string | undefined;
|
|
118
|
+
if (!sessionId) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (request.session.changeSet.getElements().length < 1) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const workspaceUris = new Set<URI>();
|
|
126
|
+
request.session.changeSet.getElements()
|
|
127
|
+
.filter((element): element is ChangeSetFileElement => element instanceof ChangeSetFileElement)
|
|
128
|
+
.map(element => this.workspaceService.getWorkspaceRootUri(element.uri))
|
|
129
|
+
.filter((element): element is URI => element !== undefined)
|
|
130
|
+
.forEach(element => workspaceUris.add(element));
|
|
131
|
+
|
|
132
|
+
for (const workspaceUri of workspaceUris) {
|
|
133
|
+
const backupLocation = this.getLocation(workspaceUri).resolve(sessionId);
|
|
134
|
+
try {
|
|
135
|
+
await this.fileService.delete(backupLocation, { recursive: true });
|
|
136
|
+
} catch (error) {
|
|
137
|
+
// Ignore cleanup errors - not critical
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { ChatAgent } from '@theia/ai-chat';
|
|
18
|
+
import { ChatResponsePartRenderer } from '@theia/ai-chat-ui/lib/browser/chat-response-part-renderer';
|
|
19
|
+
import { Agent } from '@theia/ai-core';
|
|
20
|
+
import { CommandContribution, PreferenceContribution } from '@theia/core';
|
|
21
|
+
import { FrontendApplicationContribution, RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser';
|
|
22
|
+
import { ContainerModule } from '@theia/core/shared/inversify';
|
|
23
|
+
import '../../src/browser/style/claude-code-tool-renderers.css';
|
|
24
|
+
import {
|
|
25
|
+
CLAUDE_CODE_SERVICE_PATH,
|
|
26
|
+
ClaudeCodeClient,
|
|
27
|
+
ClaudeCodeService
|
|
28
|
+
} from '../common/claude-code-service';
|
|
29
|
+
import { ClaudeCodePreferencesSchema } from '../common/claude-code-preferences';
|
|
30
|
+
import { ClaudeCodeChatAgent } from './claude-code-chat-agent';
|
|
31
|
+
import { ClaudeCodeEditToolService, ClaudeCodeEditToolServiceImpl } from './claude-code-edit-tool-service';
|
|
32
|
+
import { FileEditBackupService, FileEditBackupServiceImpl } from './claude-code-file-edit-backup-service';
|
|
33
|
+
import { ClaudeCodeClientImpl, ClaudeCodeFrontendService } from './claude-code-frontend-service';
|
|
34
|
+
import { BashToolRenderer } from './renderers/bash-tool-renderer';
|
|
35
|
+
import { EditToolRenderer } from './renderers/edit-tool-renderer';
|
|
36
|
+
import { GlobToolRenderer } from './renderers/glob-tool-renderer';
|
|
37
|
+
import { GrepToolRenderer } from './renderers/grep-tool-renderer';
|
|
38
|
+
import { LSToolRenderer } from './renderers/ls-tool-renderer';
|
|
39
|
+
import { MultiEditToolRenderer } from './renderers/multiedit-tool-renderer';
|
|
40
|
+
import { ReadToolRenderer } from './renderers/read-tool-renderer';
|
|
41
|
+
import { TodoWriteRenderer } from './renderers/todo-write-renderer';
|
|
42
|
+
import { WebFetchToolRenderer } from './renderers/web-fetch-tool-renderer';
|
|
43
|
+
import { WriteToolRenderer } from './renderers/write-tool-renderer';
|
|
44
|
+
import { ClaudeCodeSlashCommandsContribution } from './claude-code-slash-commands-contribution';
|
|
45
|
+
import { ClaudeCodeCommandContribution } from './claude-code-command-contribution';
|
|
46
|
+
|
|
47
|
+
export default new ContainerModule(bind => {
|
|
48
|
+
bind(PreferenceContribution).toConstantValue({ schema: ClaudeCodePreferencesSchema });
|
|
49
|
+
bind(FrontendApplicationContribution).to(ClaudeCodeSlashCommandsContribution).inSingletonScope();
|
|
50
|
+
bind(CommandContribution).to(ClaudeCodeCommandContribution).inSingletonScope();
|
|
51
|
+
|
|
52
|
+
bind(ClaudeCodeFrontendService).toSelf().inSingletonScope();
|
|
53
|
+
bind(ClaudeCodeClientImpl).toSelf().inSingletonScope();
|
|
54
|
+
bind(ClaudeCodeClient).toService(ClaudeCodeClientImpl);
|
|
55
|
+
bind(ClaudeCodeService).toDynamicValue(ctx => {
|
|
56
|
+
const connection = ctx.container.get<ServiceConnectionProvider>(RemoteConnectionProvider);
|
|
57
|
+
const backendClient: ClaudeCodeClient = ctx.container.get(ClaudeCodeClient);
|
|
58
|
+
return connection.createProxy(CLAUDE_CODE_SERVICE_PATH, backendClient);
|
|
59
|
+
}).inSingletonScope();
|
|
60
|
+
|
|
61
|
+
bind(FileEditBackupServiceImpl).toSelf().inSingletonScope();
|
|
62
|
+
bind(FileEditBackupService).toService(FileEditBackupServiceImpl);
|
|
63
|
+
|
|
64
|
+
bind(ClaudeCodeEditToolServiceImpl).toSelf().inSingletonScope();
|
|
65
|
+
bind(ClaudeCodeEditToolService).toService(ClaudeCodeEditToolServiceImpl);
|
|
66
|
+
|
|
67
|
+
bind(ClaudeCodeChatAgent).toSelf().inSingletonScope();
|
|
68
|
+
bind(Agent).toService(ClaudeCodeChatAgent);
|
|
69
|
+
bind(ChatAgent).toService(ClaudeCodeChatAgent);
|
|
70
|
+
|
|
71
|
+
bind(TodoWriteRenderer).toSelf().inSingletonScope();
|
|
72
|
+
bind(ChatResponsePartRenderer).toService(TodoWriteRenderer);
|
|
73
|
+
|
|
74
|
+
bind(ReadToolRenderer).toSelf().inSingletonScope();
|
|
75
|
+
bind(ChatResponsePartRenderer).toService(ReadToolRenderer);
|
|
76
|
+
|
|
77
|
+
bind(BashToolRenderer).toSelf().inSingletonScope();
|
|
78
|
+
bind(ChatResponsePartRenderer).toService(BashToolRenderer);
|
|
79
|
+
|
|
80
|
+
bind(LSToolRenderer).toSelf().inSingletonScope();
|
|
81
|
+
bind(ChatResponsePartRenderer).toService(LSToolRenderer);
|
|
82
|
+
|
|
83
|
+
bind(EditToolRenderer).toSelf().inSingletonScope();
|
|
84
|
+
bind(ChatResponsePartRenderer).toService(EditToolRenderer);
|
|
85
|
+
|
|
86
|
+
bind(GrepToolRenderer).toSelf().inSingletonScope();
|
|
87
|
+
bind(ChatResponsePartRenderer).toService(GrepToolRenderer);
|
|
88
|
+
|
|
89
|
+
bind(GlobToolRenderer).toSelf().inSingletonScope();
|
|
90
|
+
bind(ChatResponsePartRenderer).toService(GlobToolRenderer);
|
|
91
|
+
|
|
92
|
+
bind(WriteToolRenderer).toSelf().inSingletonScope();
|
|
93
|
+
bind(ChatResponsePartRenderer).toService(WriteToolRenderer);
|
|
94
|
+
|
|
95
|
+
bind(MultiEditToolRenderer).toSelf().inSingletonScope();
|
|
96
|
+
bind(ChatResponsePartRenderer).toService(MultiEditToolRenderer);
|
|
97
|
+
|
|
98
|
+
bind(WebFetchToolRenderer).toSelf().inSingletonScope();
|
|
99
|
+
bind(ChatResponsePartRenderer).toService(WebFetchToolRenderer);
|
|
100
|
+
});
|