@opensumi/ide-ai-native 3.8.1-next-1741092802.0 → 3.8.1-next-1741093151.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.
Files changed (64) hide show
  1. package/lib/browser/chat/chat-manager.service.d.ts.map +1 -1
  2. package/lib/browser/chat/chat-manager.service.js +4 -0
  3. package/lib/browser/chat/chat-manager.service.js.map +1 -1
  4. package/lib/browser/chat/chat.view.d.ts.map +1 -1
  5. package/lib/browser/chat/chat.view.js +7 -1
  6. package/lib/browser/chat/chat.view.js.map +1 -1
  7. package/lib/browser/components/chat-history.module.less +1 -0
  8. package/lib/browser/mcp/base-apply.service.d.ts +18 -7
  9. package/lib/browser/mcp/base-apply.service.d.ts.map +1 -1
  10. package/lib/browser/mcp/base-apply.service.js +185 -65
  11. package/lib/browser/mcp/base-apply.service.js.map +1 -1
  12. package/lib/browser/mcp/tools/components/EditFile.d.ts.map +1 -1
  13. package/lib/browser/mcp/tools/components/EditFile.js +15 -9
  14. package/lib/browser/mcp/tools/components/EditFile.js.map +1 -1
  15. package/lib/browser/mcp/tools/components/index.module.less +3 -0
  16. package/lib/browser/mcp/tools/createNewFileWithText.d.ts +1 -0
  17. package/lib/browser/mcp/tools/createNewFileWithText.d.ts.map +1 -1
  18. package/lib/browser/mcp/tools/createNewFileWithText.js +18 -11
  19. package/lib/browser/mcp/tools/createNewFileWithText.js.map +1 -1
  20. package/lib/browser/mcp/tools/handlers/EditFile.js +1 -1
  21. package/lib/browser/mcp/tools/handlers/EditFile.js.map +1 -1
  22. package/lib/browser/model/msg-history-manager.d.ts +1 -0
  23. package/lib/browser/model/msg-history-manager.d.ts.map +1 -1
  24. package/lib/browser/model/msg-history-manager.js +3 -0
  25. package/lib/browser/model/msg-history-manager.js.map +1 -1
  26. package/lib/browser/widget/inline-diff/inline-diff-manager.d.ts.map +1 -1
  27. package/lib/browser/widget/inline-diff/inline-diff-manager.js +68 -8
  28. package/lib/browser/widget/inline-diff/inline-diff-manager.js.map +1 -1
  29. package/lib/browser/widget/inline-diff/inline-diff-previewer.d.ts +10 -4
  30. package/lib/browser/widget/inline-diff/inline-diff-previewer.d.ts.map +1 -1
  31. package/lib/browser/widget/inline-diff/inline-diff-previewer.js +14 -3
  32. package/lib/browser/widget/inline-diff/inline-diff-previewer.js.map +1 -1
  33. package/lib/browser/widget/inline-diff/inline-diff-widget.module.less +25 -4
  34. package/lib/browser/widget/inline-diff/inline-diff.controller.d.ts +3 -3
  35. package/lib/browser/widget/inline-diff/inline-diff.controller.d.ts.map +1 -1
  36. package/lib/browser/widget/inline-diff/inline-diff.controller.js +10 -5
  37. package/lib/browser/widget/inline-diff/inline-diff.controller.js.map +1 -1
  38. package/lib/browser/widget/inline-stream-diff/inline-stream-diff.handler.d.ts +46 -17
  39. package/lib/browser/widget/inline-stream-diff/inline-stream-diff.handler.d.ts.map +1 -1
  40. package/lib/browser/widget/inline-stream-diff/inline-stream-diff.handler.js +110 -53
  41. package/lib/browser/widget/inline-stream-diff/inline-stream-diff.handler.js.map +1 -1
  42. package/lib/browser/widget/inline-stream-diff/live-preview.decoration.d.ts +4 -0
  43. package/lib/browser/widget/inline-stream-diff/live-preview.decoration.d.ts.map +1 -1
  44. package/lib/browser/widget/inline-stream-diff/live-preview.decoration.js +26 -1
  45. package/lib/browser/widget/inline-stream-diff/live-preview.decoration.js.map +1 -1
  46. package/lib/common/types.d.ts +1 -0
  47. package/lib/common/types.d.ts.map +1 -1
  48. package/package.json +23 -23
  49. package/src/browser/chat/chat-manager.service.ts +6 -0
  50. package/src/browser/chat/chat.view.tsx +7 -2
  51. package/src/browser/components/chat-history.module.less +1 -0
  52. package/src/browser/mcp/base-apply.service.ts +222 -67
  53. package/src/browser/mcp/tools/components/EditFile.tsx +16 -9
  54. package/src/browser/mcp/tools/components/index.module.less +3 -0
  55. package/src/browser/mcp/tools/createNewFileWithText.ts +20 -12
  56. package/src/browser/mcp/tools/handlers/EditFile.ts +1 -1
  57. package/src/browser/model/msg-history-manager.ts +4 -0
  58. package/src/browser/widget/inline-diff/inline-diff-manager.tsx +143 -21
  59. package/src/browser/widget/inline-diff/inline-diff-previewer.ts +25 -7
  60. package/src/browser/widget/inline-diff/inline-diff-widget.module.less +25 -4
  61. package/src/browser/widget/inline-diff/inline-diff.controller.ts +16 -8
  62. package/src/browser/widget/inline-stream-diff/inline-stream-diff.handler.tsx +139 -68
  63. package/src/browser/widget/inline-stream-diff/live-preview.decoration.tsx +30 -1
  64. package/src/common/types.ts +1 -0
@@ -6,12 +6,14 @@ import { WorkbenchEditorService } from '@opensumi/ide-editor';
6
6
  import {
7
7
  EditorGroupCloseEvent,
8
8
  EditorGroupOpenEvent,
9
+ IEditorDocumentModelService,
9
10
  RegisterEditorSideComponentEvent,
10
11
  } from '@opensumi/ide-editor/lib/browser';
11
12
  import { IMarkerService } from '@opensumi/ide-markers';
12
- import { ICodeEditor, Position, Range, Selection, SelectionDirection } from '@opensumi/ide-monaco';
13
- import { Deferred, Emitter, URI, path } from '@opensumi/ide-utils';
13
+ import { ICodeEditor, ITextModel, Position, Range, Selection, SelectionDirection } from '@opensumi/ide-monaco';
14
+ import { Deferred, DisposableMap, Emitter, IDisposable, URI, path } from '@opensumi/ide-utils';
14
15
  import { SumiReadableStream } from '@opensumi/ide-utils/lib/stream';
16
+ import { EditOperation } from '@opensumi/monaco-editor-core/esm/vs/editor/common/core/editOperation';
15
17
 
16
18
  import { IChatInternalService } from '../../common';
17
19
  import { CodeBlockData, CodeBlockStatus } from '../../common/types';
@@ -23,15 +25,9 @@ import {
23
25
  InlineDiffService,
24
26
  LiveInlineDiffPreviewer,
25
27
  } from '../widget/inline-diff';
26
- import { InlineStreamDiffHandler } from '../widget/inline-stream-diff/inline-stream-diff.handler';
28
+ import { BaseInlineStreamDiffHandler } from '../widget/inline-stream-diff/inline-stream-diff.handler';
27
29
 
28
- import { FileHandler } from './tools/handlers/ReadFile';
29
-
30
- // 提供代码块的唯一索引,迭代轮次,生成状态管理(包括取消),关联文件位置这些信息的记录,后续并行 apply 的支持
31
30
  export abstract class BaseApplyService extends WithEventBus {
32
- @Autowired(FileHandler)
33
- protected fileHandler: FileHandler;
34
-
35
31
  @Autowired(IChatInternalService)
36
32
  protected chatInternalService: ChatInternalService;
37
33
 
@@ -47,9 +43,14 @@ export abstract class BaseApplyService extends WithEventBus {
47
43
  @Autowired(IMarkerService)
48
44
  private readonly markerService: IMarkerService;
49
45
 
46
+ @Autowired(IEditorDocumentModelService)
47
+ private readonly editorDocumentModelService: IEditorDocumentModelService;
48
+
50
49
  private onCodeBlockUpdateEmitter = new Emitter<CodeBlockData>();
51
50
  public onCodeBlockUpdate = this.onCodeBlockUpdateEmitter.event;
52
51
 
52
+ private currentSessionId?: string;
53
+
53
54
  constructor() {
54
55
  super();
55
56
  this.addDispose(
@@ -57,6 +58,15 @@ export abstract class BaseApplyService extends WithEventBus {
57
58
  this.cancelAllApply();
58
59
  }),
59
60
  );
61
+ this.currentSessionId = this.chatInternalService.sessionModel.sessionId;
62
+ this.addDispose(
63
+ this.chatInternalService.onChangeSession((sessionId) => {
64
+ if (sessionId !== this.currentSessionId) {
65
+ this.cancelAllApply();
66
+ this.currentSessionId = sessionId;
67
+ }
68
+ }),
69
+ );
60
70
  this.addDispose(
61
71
  this.chatInternalService.onRegenerateRequest(() => {
62
72
  const messages = this.chatInternalService.sessionModel.history.getMessages();
@@ -85,30 +95,69 @@ export abstract class BaseApplyService extends WithEventBus {
85
95
  return message?.codeBlockMap;
86
96
  }
87
97
 
88
- private activePreviewer: BaseInlineDiffPreviewer<InlineStreamDiffHandler> | undefined;
98
+ private getSessionCodeBlocksForPath(relativePath: string, sessionId?: string) {
99
+ sessionId = sessionId || this.chatInternalService.sessionModel.sessionId;
100
+ const sessionModel = this.chatInternalService.getSession(sessionId);
101
+ if (!sessionModel) {
102
+ throw new Error(`Session ${sessionId} not found`);
103
+ }
104
+ const sessionAdditionals = sessionModel.history.sessionAdditionals;
105
+ const codeBlocks: CodeBlockData[] = Array.from(sessionAdditionals.values())
106
+ .map((additional) => additional.codeBlockMap as { [toolCallId: string]: CodeBlockData })
107
+ .reduce((acc, cur) => {
108
+ Object.values(cur).forEach((block) => {
109
+ if (block.relativePath === relativePath) {
110
+ acc.push(block);
111
+ }
112
+ });
113
+ return acc;
114
+ }, [] as CodeBlockData[])
115
+ .sort((a, b) => b.version - a.version);
116
+ return codeBlocks;
117
+ }
118
+
119
+ private activePreviewerMap = this.registerDispose(
120
+ new DisposableMap<string, BaseInlineDiffPreviewer<BaseInlineStreamDiffHandler>>(),
121
+ );
122
+
123
+ private editorListenerMap = this.registerDispose(new DisposableMap<string, IDisposable>());
89
124
 
90
125
  @OnEvent(EditorGroupCloseEvent)
91
126
  onEditorGroupClose(event: EditorGroupCloseEvent) {
92
- if (this.activePreviewer?.getNode()?.uri.path.toString() === event.payload.resource.uri.path.toString()) {
93
- this.activePreviewer.dispose();
94
- this.activePreviewer = undefined;
127
+ const relativePath = path.relative(this.appConfig.workspaceDir, event.payload.resource.uri.path.toString());
128
+ const activePreviewer = this.activePreviewerMap.get(relativePath);
129
+ if (activePreviewer) {
130
+ this.activePreviewerMap.disposeKey(relativePath);
95
131
  }
132
+ this.editorListenerMap.disposeKey(event.payload.resource.uri.toString());
96
133
  }
97
134
 
98
135
  @OnEvent(EditorGroupOpenEvent)
99
136
  async onEditorGroupOpen(event: EditorGroupOpenEvent) {
100
- if (!this.chatInternalService.sessionModel.history.getMessages().length) {
137
+ const relativePath = path.relative(this.appConfig.workspaceDir, event.payload.resource.uri.path.toString());
138
+ if (
139
+ this.duringApply ||
140
+ this.activePreviewerMap.has(relativePath) ||
141
+ !this.chatInternalService.sessionModel.history.getMessages().length
142
+ ) {
101
143
  return;
102
144
  }
103
- const relativePath = path.relative(this.appConfig.workspaceDir, event.payload.resource.uri.path.toString());
104
145
  const filePendingApplies = Object.values(
105
146
  this.getMessageCodeBlocks(this.chatInternalService.sessionModel.history.lastMessageId!) || {},
106
147
  ).filter((block) => block.relativePath === relativePath && block.status === 'pending');
107
148
  // TODO: 刷新后重新应用,事件无法恢复 & 恢复继续请求,需要改造成批量apply形式
108
- // TODO: 暂时只支持 pending 串行的 apply,后续支持批量apply后统一accept
109
- if (filePendingApplies.length > 0) {
110
- this.renderApplyResult(filePendingApplies[0], filePendingApplies[0].updatedCode!);
149
+ if (filePendingApplies.length > 0 && filePendingApplies[0].updatedCode) {
150
+ const editor = event.payload.group.codeEditor.monacoEditor;
151
+ this.renderApplyResult(editor, filePendingApplies[0], filePendingApplies[0].updatedCode);
152
+ }
153
+ }
154
+
155
+ get currentPreviewer() {
156
+ const currentUri = this.editorService.currentEditor?.currentUri;
157
+ if (!currentUri) {
158
+ return undefined;
111
159
  }
160
+ return this.activePreviewerMap.get(path.relative(this.appConfig.workspaceDir, currentUri.path.toString()));
112
161
  }
113
162
 
114
163
  getUriPendingCodeBlock(uri: URI): CodeBlockData | undefined {
@@ -127,6 +176,25 @@ export abstract class BaseApplyService extends WithEventBus {
127
176
  );
128
177
  }
129
178
 
179
+ getPendingPaths(sessionId?: string): string[] {
180
+ sessionId = sessionId || this.chatInternalService.sessionModel.sessionId;
181
+ const sessionModel = this.chatInternalService.getSession(sessionId);
182
+ if (!sessionModel) {
183
+ throw new Error(`Session ${sessionId} not found`);
184
+ }
185
+ const sessionAdditionals = sessionModel.history.sessionAdditionals;
186
+ return Array.from(sessionAdditionals.values())
187
+ .map((additional) => additional.codeBlockMap as { [toolCallId: string]: CodeBlockData })
188
+ .reduce((acc, cur) => {
189
+ Object.values(cur).forEach((block) => {
190
+ if (block.status === 'pending' && !acc.includes(block.relativePath)) {
191
+ acc.push(block.relativePath);
192
+ }
193
+ });
194
+ return acc;
195
+ }, [] as string[]);
196
+ }
197
+
130
198
  getCodeBlock(toolCallId: string, messageId?: string): CodeBlockData | undefined {
131
199
  messageId = messageId || this.chatInternalService.sessionModel.history.lastMessageId;
132
200
  if (!messageId) {
@@ -155,9 +223,12 @@ export abstract class BaseApplyService extends WithEventBus {
155
223
  this.onCodeBlockUpdateEmitter.fire(codeBlock);
156
224
  }
157
225
 
158
- registerCodeBlock(relativePath: string, content: string, toolCallId: string): CodeBlockData {
226
+ async registerCodeBlock(relativePath: string, content: string, toolCallId: string): Promise<CodeBlockData> {
159
227
  const lastMessageId = this.chatInternalService.sessionModel.history.lastMessageId!;
160
228
  const savedCodeBlockMap = this.getMessageCodeBlocks(lastMessageId) || {};
229
+ const originalModelRef = await this.editorDocumentModelService.createModelReference(
230
+ URI.file(path.join(this.appConfig.workspaceDir, relativePath)),
231
+ );
161
232
  const newBlock: CodeBlockData = {
162
233
  codeEdit: content,
163
234
  relativePath,
@@ -166,11 +237,13 @@ export abstract class BaseApplyService extends WithEventBus {
166
237
  version: 1,
167
238
  createdAt: Date.now(),
168
239
  toolCallId,
240
+ // TODO: 支持range
241
+ originalCode: originalModelRef.instance.getText(),
169
242
  };
170
243
  const samePathCodeBlocks = Object.values(savedCodeBlockMap).filter((block) => block.relativePath === relativePath);
171
244
  if (samePathCodeBlocks.length > 0) {
172
245
  newBlock.version = samePathCodeBlocks.length;
173
- for (const block of samePathCodeBlocks.sort((a, b) => b.version - a.version)) {
246
+ for (const block of samePathCodeBlocks.sort((a, b) => a.version - b.version)) {
174
247
  // 如果连续的上一个同文件apply结果存在LintError,则iterationCount++
175
248
  if (block.relativePath === relativePath && block.applyResult?.diagnosticInfos?.length) {
176
249
  newBlock.iterationCount++;
@@ -187,21 +260,40 @@ export abstract class BaseApplyService extends WithEventBus {
187
260
  return newBlock;
188
261
  }
189
262
 
263
+ private duringApply?: boolean;
264
+
190
265
  /**
191
266
  * Apply changes of a code block
192
267
  */
193
268
  async apply(codeBlock: CodeBlockData): Promise<CodeBlockData> {
194
269
  try {
270
+ this.duringApply = true;
195
271
  if (codeBlock.iterationCount > 3) {
196
272
  throw new Error('Lint error max iteration count exceeded');
197
273
  }
198
- const fastApplyFileResult = await this.doApply(codeBlock);
274
+ // 新建文件场景,直接返回codeEdit
275
+ const fastApplyFileResult = !codeBlock.originalCode
276
+ ? {
277
+ result: codeBlock.codeEdit,
278
+ }
279
+ : await this.doApply(codeBlock);
199
280
  if (!fastApplyFileResult.stream && !fastApplyFileResult.result) {
200
281
  throw new Error('No apply content provided');
201
282
  }
202
283
 
284
+ if (this.activePreviewerMap.has(codeBlock.relativePath)) {
285
+ this.activePreviewerMap.disposeKey(codeBlock.relativePath);
286
+ }
287
+ // FIXME: 同一个bubble单个文件多次写入(如迭代)兼容
203
288
  // trigger diffPreivewer & return expected diff result directly
289
+ const result = await this.editorService.open(
290
+ URI.file(path.join(this.appConfig.workspaceDir, codeBlock.relativePath)),
291
+ );
292
+ if (!result) {
293
+ throw new Error('Failed to open file');
294
+ }
204
295
  const applyResult = await this.renderApplyResult(
296
+ result.group.codeEditor.monacoEditor,
205
297
  codeBlock,
206
298
  (fastApplyFileResult.result || fastApplyFileResult.stream)!,
207
299
  fastApplyFileResult.range,
@@ -217,31 +309,39 @@ export abstract class BaseApplyService extends WithEventBus {
217
309
  codeBlock.status = 'failed';
218
310
  this.updateCodeBlock(codeBlock);
219
311
  throw err;
312
+ } finally {
313
+ this.duringApply = false;
220
314
  }
221
315
  }
222
316
 
223
317
  async renderApplyResult(
318
+ editor: ICodeEditor,
224
319
  codeBlock: CodeBlockData,
225
320
  updatedContentOrStream: string | SumiReadableStream<IChatProgress>,
226
321
  range?: Range,
227
322
  ): Promise<{ diff: string; diagnosticInfos: IMarker[] } | undefined> {
228
- const { relativePath } = codeBlock;
229
- const openResult = await this.editorService.open(URI.file(path.join(this.appConfig.workspaceDir, relativePath)));
230
- if (!openResult) {
231
- throw new Error('Failed to open editor');
232
- }
233
- const editor = openResult.group.codeEditor.monacoEditor;
323
+ const deferred = new Deferred<{ diff: string; diagnosticInfos: IMarker[] }>();
234
324
  const inlineDiffController = InlineDiffController.get(editor)!;
235
- codeBlock.status = 'pending';
236
- // 强刷展示 manager 视图
237
- this.eventBus.fire(new RegisterEditorSideComponentEvent());
238
- this.updateCodeBlock(codeBlock);
239
-
240
- const fullOriginalContent = editor.getModel()!.getValue();
241
- range = range || editor.getModel()?.getFullModelRange()!;
242
- // const savedRangeContent = editor.getModel()!.getValueInRange(range);
325
+ range = range || editor.getModel()!.getFullModelRange();
243
326
 
244
327
  if (typeof updatedContentOrStream === 'string') {
328
+ const editorCurrentContent = editor.getModel()!.getValue();
329
+ const document = this.editorDocumentModelService.getModelReference(
330
+ URI.file(path.join(this.appConfig.workspaceDir, codeBlock.relativePath)),
331
+ );
332
+ if (editorCurrentContent !== updatedContentOrStream || document?.instance.dirty) {
333
+ editor.getModel()?.pushEditOperations([], [EditOperation.replace(range, updatedContentOrStream)], () => null);
334
+ await this.editorService.save(URI.file(path.join(this.appConfig.workspaceDir, codeBlock.relativePath)));
335
+ }
336
+ const earlistPendingCodeBlock = this.getSessionCodeBlocksForPath(codeBlock.relativePath).find(
337
+ (block) => block.status === 'pending',
338
+ );
339
+ if ((earlistPendingCodeBlock?.originalCode || codeBlock.originalCode) === updatedContentOrStream) {
340
+ codeBlock.status = 'cancelled';
341
+ this.updateCodeBlock(codeBlock);
342
+ deferred.resolve();
343
+ return;
344
+ }
245
345
  // Create diff previewer
246
346
  const previewer = inlineDiffController.createDiffPreviewer(
247
347
  editor,
@@ -249,18 +349,41 @@ export abstract class BaseApplyService extends WithEventBus {
249
349
  {
250
350
  disposeWhenEditorClosed: true,
251
351
  renderRemovedWidgetImmediately: true,
352
+ reverse: true,
252
353
  },
253
354
  ) as LiveInlineDiffPreviewer;
254
- // TODO: 支持多个diffPreviewer
255
- this.activePreviewer = previewer;
355
+ this.activePreviewerMap.set(codeBlock.relativePath, previewer);
256
356
  codeBlock.updatedCode = updatedContentOrStream;
257
- previewer.setValue(updatedContentOrStream);
357
+ codeBlock.status = 'pending';
358
+ this.updateCodeBlock(codeBlock);
359
+ // 新建文件场景,为避免model为空,加一个空行
360
+ previewer.setValue(earlistPendingCodeBlock?.originalCode || codeBlock.originalCode || '\n');
361
+ // 强刷展示 manager 视图
362
+ this.eventBus.fire(new RegisterEditorSideComponentEvent());
363
+
364
+ this.listenPartialEdit(editor.getModel()!, codeBlock).then((result) => {
365
+ if (result) {
366
+ codeBlock.applyResult = result;
367
+ }
368
+ this.updateCodeBlock(codeBlock);
369
+ this.editorService.save(URI.file(path.join(this.appConfig.workspaceDir, codeBlock.relativePath)));
370
+ });
371
+
372
+ const { diff, rangesFromDiffHunk } = this.getDiffResult(
373
+ codeBlock.originalCode,
374
+ codeBlock.updatedCode,
375
+ codeBlock.relativePath,
376
+ );
377
+ const diagnosticInfos = this.getDiagnosticInfos(editor.getModel()!.uri.toString(), rangesFromDiffHunk);
378
+ deferred.resolve({
379
+ diff,
380
+ diagnosticInfos,
381
+ });
258
382
  } else {
259
383
  const controller = new InlineChatController();
260
384
  controller.mountReadable(updatedContentOrStream);
261
- const inlineDiffHandler = InlineDiffController.get(editor)!;
262
385
 
263
- this.activePreviewer = inlineDiffHandler.showPreviewerByStream(editor, {
386
+ const previewer = inlineDiffController.showPreviewerByStream(editor, {
264
387
  crossSelection: Selection.fromRange(range, SelectionDirection.LTR),
265
388
  chatResponse: controller,
266
389
  previewerOptions: {
@@ -269,14 +392,27 @@ export abstract class BaseApplyService extends WithEventBus {
269
392
  },
270
393
  }) as LiveInlineDiffPreviewer;
271
394
  this.addDispose(
272
- this.activePreviewer.getNode()!.onDiffFinished((diffModel) => {
395
+ // 流式输出结束后,转为直接输出逻辑
396
+ previewer.getNode()!.onDiffFinished(async (diffModel) => {
273
397
  codeBlock.updatedCode = diffModel.newFullRangeTextLines.join('\n');
398
+ // TODO: 添加 reapply
399
+ // 实际应用结果为空,则取消
400
+ if (codeBlock.updatedCode === codeBlock.originalCode) {
401
+ codeBlock.status = 'cancelled';
402
+ this.updateCodeBlock(codeBlock);
403
+ previewer.dispose();
404
+ deferred.resolve();
405
+ return;
406
+ }
274
407
  this.updateCodeBlock(codeBlock);
408
+ previewer.dispose();
409
+ const result = await this.renderApplyResult(editor, codeBlock, codeBlock.updatedCode);
410
+ deferred.resolve(result);
275
411
  }),
276
412
  );
413
+ this.activePreviewerMap.set(codeBlock.relativePath, previewer);
277
414
  }
278
-
279
- return this.listenPartialEdit(editor, codeBlock, fullOriginalContent);
415
+ return deferred.promise;
280
416
  }
281
417
 
282
418
  /**
@@ -284,9 +420,12 @@ export abstract class BaseApplyService extends WithEventBus {
284
420
  */
285
421
  cancelApply(blockData: CodeBlockData): void {
286
422
  if (blockData.status === 'generating' || blockData.status === 'pending') {
287
- if (this.activePreviewer) {
288
- this.activePreviewer.getNode()?.livePreviewDiffDecorationModel.discardUnProcessed();
289
- this.activePreviewer.dispose();
423
+ if (this.activePreviewerMap.has(blockData.relativePath)) {
424
+ this.activePreviewerMap
425
+ .get(blockData.relativePath)
426
+ ?.getNode()
427
+ ?.livePreviewDiffDecorationModel.discardUnProcessed();
428
+ this.activePreviewerMap.disposeKey(blockData.relativePath);
290
429
  }
291
430
  blockData.status = 'cancelled';
292
431
  this.updateCodeBlock(blockData);
@@ -327,7 +466,9 @@ export abstract class BaseApplyService extends WithEventBus {
327
466
  if (!codeBlock) {
328
467
  throw new Error('No pending code block found');
329
468
  }
330
- const decorationModel = this.activePreviewer?.getNode()?.livePreviewDiffDecorationModel;
469
+ const decorationModel = this.activePreviewerMap
470
+ .get(codeBlock.relativePath)
471
+ ?.getNode()?.livePreviewDiffDecorationModel;
331
472
  if (!decorationModel) {
332
473
  throw new Error('No active previewer found');
333
474
  }
@@ -341,32 +482,27 @@ export abstract class BaseApplyService extends WithEventBus {
341
482
  this.updateCodeBlock(codeBlock);
342
483
  }
343
484
 
344
- protected listenPartialEdit(editor: ICodeEditor, codeBlock: CodeBlockData, fullOriginalContent: string) {
485
+ protected listenPartialEdit(model: ITextModel, codeBlock: CodeBlockData) {
345
486
  const deferred = new Deferred<{ diff: string; diagnosticInfos: IMarker[] }>();
487
+ const uriString = model.uri.toString();
346
488
  const toDispose = this.inlineDiffService.onPartialEdit((event) => {
347
489
  // TODO 支持自动保存
348
- if (event.totalPartialEditCount === event.resolvedPartialEditCount) {
490
+ if (
491
+ event.totalPartialEditCount === event.resolvedPartialEditCount &&
492
+ event.uri.path === model.uri.path.toString()
493
+ ) {
349
494
  if (event.acceptPartialEditCount > 0) {
350
495
  codeBlock.status = 'success';
351
- const appliedResult = editor.getModel()!.getValue();
352
- const diffResult = createPatch(codeBlock.relativePath, fullOriginalContent, appliedResult)
353
- .split('\n')
354
- .slice(4)
355
- .join('\n');
356
- const rangesFromDiffHunk = diffResult
357
- .split('\n')
358
- .map((line) => {
359
- if (line.startsWith('@@')) {
360
- const [, , , start, end] = line.match(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@/)!;
361
- return new Range(parseInt(start, 10), 0, parseInt(end, 10), 0);
362
- }
363
- return null;
364
- })
365
- .filter((range) => range !== null);
366
- const diagnosticInfos = this.getDiagnosticInfos(editor.getModel()!.uri.toString(), rangesFromDiffHunk);
496
+ const appliedResult = model.getValue();
497
+ const { diff, rangesFromDiffHunk } = this.getDiffResult(
498
+ codeBlock.originalCode,
499
+ appliedResult,
500
+ codeBlock.relativePath,
501
+ );
502
+ const diagnosticInfos = this.getDiagnosticInfos(model.uri.toString(), rangesFromDiffHunk);
367
503
  // 移除开头的几个固定信息,避免浪费 tokens
368
504
  deferred.resolve({
369
- diff: diffResult,
505
+ diff,
370
506
  diagnosticInfos,
371
507
  });
372
508
  } else {
@@ -374,12 +510,31 @@ export abstract class BaseApplyService extends WithEventBus {
374
510
  codeBlock.status = 'cancelled';
375
511
  deferred.resolve();
376
512
  }
377
- toDispose.dispose();
513
+ this.editorListenerMap.disposeKey(uriString);
378
514
  }
379
515
  });
516
+ this.editorListenerMap.set(uriString, toDispose);
380
517
  return deferred.promise;
381
518
  }
382
519
 
520
+ protected getDiffResult(originalContent: string, appliedResult: string, relativePath: string) {
521
+ const diffResult = createPatch(relativePath, originalContent, appliedResult).split('\n').slice(4).join('\n');
522
+ const rangesFromDiffHunk = diffResult
523
+ .split('\n')
524
+ .map((line) => {
525
+ if (line.startsWith('@@')) {
526
+ const [, , , start, end] = line.match(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@/)!;
527
+ return new Range(parseInt(start, 10), 0, parseInt(end, 10), 0);
528
+ }
529
+ return null;
530
+ })
531
+ .filter((range) => range !== null);
532
+ return {
533
+ diff: diffResult,
534
+ rangesFromDiffHunk,
535
+ };
536
+ }
537
+
383
538
  /**
384
539
  * Apply changes of a code block, return stream to render inline diff in stream mode, result to render inline diff directly
385
540
  * range is optional, if not provided, the result will be applied to the the full file
@@ -13,6 +13,7 @@ import {
13
13
  useInjectable,
14
14
  } from '@opensumi/ide-core-browser';
15
15
  import { Loading } from '@opensumi/ide-core-browser/lib/components/ai-native';
16
+ import { WorkbenchEditorService } from '@opensumi/ide-editor';
16
17
  import { ILanguageService } from '@opensumi/monaco-editor-core/esm/vs/editor/common/languages/language';
17
18
  import { IModelService } from '@opensumi/monaco-editor-core/esm/vs/editor/common/services/model';
18
19
  import { StandaloneServices } from '@opensumi/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
@@ -30,6 +31,7 @@ export const EditFileToolComponent = (props: IMCPServerToolComponentProps) => {
30
31
  const labelService = useInjectable(LabelService);
31
32
  const appConfig = useInjectable<AppConfig>(AppConfig);
32
33
  const applyService = useInjectable<BaseApplyService>(BaseApplyService);
34
+ const editorService = useInjectable<WorkbenchEditorService>(WorkbenchEditorService);
33
35
  const { target_file = '', code_edit, instructions } = args || {};
34
36
  const absolutePath = path.join(appConfig.workspaceDir, target_file);
35
37
  const [codeBlockData, setCodeBlockData] = useState<CodeBlockData | undefined>(
@@ -55,15 +57,20 @@ export const EditFileToolComponent = (props: IMCPServerToolComponentProps) => {
55
57
 
56
58
  useEffect(() => {
57
59
  const disposable = applyService.onCodeBlockUpdate((codeBlockData) => {
58
- setCodeBlockData({ ...codeBlockData });
60
+ if (codeBlockData.toolCallId === toolCallId) {
61
+ setCodeBlockData({ ...codeBlockData });
62
+ }
59
63
  });
60
64
  return () => {
61
65
  disposable.dispose();
62
66
  };
63
67
  }, []);
64
68
 
65
- // 多次迭代时,仅在首处tool组件中展示
66
- // FIXME: 这个优化有必要吗?每次都展示也挺好?
69
+ const handleToggleMode = (e: React.MouseEvent<HTMLDivElement>) => {
70
+ e.stopPropagation();
71
+ setMode(mode === 'code' ? 'diff' : 'code');
72
+ };
73
+
67
74
  if (!args || !codeBlockData) {
68
75
  return null;
69
76
  }
@@ -77,7 +84,7 @@ export const EditFileToolComponent = (props: IMCPServerToolComponentProps) => {
77
84
  })}
78
85
  onClick={() => {
79
86
  if (codeBlockData.status === 'pending') {
80
- applyService.renderApplyResult(codeBlockData, codeBlockData.updatedCode!);
87
+ editorService.open(URI.file(absolutePath));
81
88
  } else if (codeBlockData.status === 'success') {
82
89
  applyService.revealApplyPosition(codeBlockData);
83
90
  }
@@ -89,15 +96,15 @@ export const EditFileToolComponent = (props: IMCPServerToolComponentProps) => {
89
96
  {codeBlockData.iterationCount > 1 && (
90
97
  <span className={styles['edit-file-tool-iteration-count']}>{codeBlockData.iterationCount}/3</span>
91
98
  )}
92
- {renderStatus(codeBlockData)}
99
+ {renderStatus(codeBlockData, props.result)}
93
100
  </div>
94
101
  <div className={styles.right}>
95
102
  <Popover title={'Show Code'} id={'edit-file-tool-show-code'}>
96
- <Icon iconClass='codicon codicon-file-code' onClick={() => setMode('code')} />
103
+ <Icon iconClass='codicon codicon-file-code' onClick={handleToggleMode} />
97
104
  </Popover>
98
105
  {codeBlockData.applyResult?.diff && (
99
106
  <Popover title={'Show Diff'} id={'edit-file-tool-show-diff'}>
100
- <Icon iconClass='codicon codicon-diff-multiple' onClick={() => setMode('diff')} />
107
+ <Icon iconClass='codicon codicon-diff-multiple' onClick={handleToggleMode} />
101
108
  </Popover>
102
109
  )}
103
110
  </div>
@@ -131,7 +138,7 @@ export const EditFileToolComponent = (props: IMCPServerToolComponentProps) => {
131
138
  ];
132
139
  };
133
140
 
134
- const renderStatus = (codeBlockData: CodeBlockData) => {
141
+ const renderStatus = (codeBlockData: CodeBlockData, error?: string) => {
135
142
  const status = codeBlockData.status;
136
143
  switch (status) {
137
144
  case 'generating':
@@ -150,7 +157,7 @@ const renderStatus = (codeBlockData: CodeBlockData) => {
150
157
  );
151
158
  case 'failed':
152
159
  return (
153
- <Popover title='Failed' id={'edit-file-tool-status-failed'}>
160
+ <Popover title={`Failed (${error})`} id={'edit-file-tool-status-failed'}>
154
161
  <Icon iconClass='codicon codicon-error' style={{ color: 'var(--debugConsole-errorForeground)' }} />
155
162
  </Popover>
156
163
  );
@@ -17,6 +17,9 @@
17
17
  align-items: center;
18
18
  justify-content: center;
19
19
  }
20
+ .left {
21
+ overflow: hidden;
22
+ }
20
23
  .left,
21
24
  .right {
22
25
  display: flex;
@@ -6,10 +6,13 @@ import { IFileServiceClient } from '@opensumi/ide-file-service';
6
6
  import { IWorkspaceService } from '@opensumi/ide-workspace';
7
7
 
8
8
  import { IMCPServerRegistry, MCPLogger, MCPServerContribution, MCPToolDefinition } from '../../types';
9
+ import { BaseApplyService } from '../base-apply.service';
10
+
11
+ import { EditFileToolComponent } from './components/EditFile';
9
12
 
10
13
  const inputSchema = z.object({
11
- pathInProject: z.string().describe('The relative path where the file should be created'),
12
- text: z.string().describe('The content to write into the new file'),
14
+ target_file: z.string().describe('The relative path where the file should be created'),
15
+ code_edit: z.string().describe('The content to write into the new file'),
13
16
  });
14
17
 
15
18
  @Domain(MCPServerContribution)
@@ -20,8 +23,12 @@ export class CreateNewFileWithTextTool implements MCPServerContribution {
20
23
  @Autowired(IFileServiceClient)
21
24
  private readonly fileService: IFileServiceClient;
22
25
 
26
+ @Autowired(BaseApplyService)
27
+ private applyService: BaseApplyService;
28
+
23
29
  registerMCPServer(registry: IMCPServerRegistry): void {
24
30
  registry.registerMCPTool(this.getToolDefinition());
31
+ registry.registerToolComponent('create_new_file_with_text', EditFileToolComponent);
25
32
  }
26
33
 
27
34
  getToolDefinition(): MCPToolDefinition {
@@ -31,19 +38,16 @@ export class CreateNewFileWithTextTool implements MCPServerContribution {
31
38
  description:
32
39
  'Creates a new file at the specified path within the project directory and populates it with the provided text. ' +
33
40
  'Use this tool to generate new files in your project structure. ' +
34
- 'Requires two parameters: ' +
35
- '- pathInProject: The relative path where the file should be created ' +
36
- '- text: The content to write into the new file ' +
37
41
  'Returns one of two possible responses: ' +
38
42
  '"ok" if the file was successfully created and populated, ' +
39
43
  '"can\'t find project dir" if the project directory cannot be determined. ' +
40
- 'Note: Creates any necessary parent directories automatically.',
44
+ 'Note: This tool creates any necessary parent directories automatically.',
41
45
  inputSchema,
42
46
  handler: this.handler.bind(this),
43
47
  };
44
48
  }
45
49
 
46
- private async handler(args: z.infer<typeof inputSchema>, logger: MCPLogger) {
50
+ private async handler(args: z.infer<typeof inputSchema> & { toolCallId: string }, logger: MCPLogger) {
47
51
  try {
48
52
  // 获取工作区根目录
49
53
  const workspaceRoots = this.workspaceService.tryGetRoots();
@@ -57,7 +61,7 @@ export class CreateNewFileWithTextTool implements MCPServerContribution {
57
61
 
58
62
  // 构建完整的文件路径
59
63
  const rootUri = URI.parse(workspaceRoots[0].uri);
60
- const fullPath = path.join(rootUri.codeUri.fsPath, args.pathInProject);
64
+ const fullPath = path.join(rootUri.codeUri.fsPath, args.target_file);
61
65
  const fileUri = URI.file(fullPath);
62
66
 
63
67
  // 创建父目录
@@ -65,17 +69,21 @@ export class CreateNewFileWithTextTool implements MCPServerContribution {
65
69
  const parentUri = URI.file(parentDir);
66
70
  await this.fileService.createFolder(parentUri.toString());
67
71
 
68
- // 写入文件内容
69
- await this.fileService.createFile(fileUri.toString(), { content: args.text });
72
+ // 创建文件
73
+ await this.fileService.createFile(fileUri.toString());
74
+
75
+ // 使用 applyService 写入文件内容
76
+ const codeBlock = await this.applyService.registerCodeBlock(args.target_file, args.code_edit, args.toolCallId);
77
+ await this.applyService.apply(codeBlock);
70
78
 
71
- logger.appendLine(`Successfully created file at: ${args.pathInProject}`);
79
+ logger.appendLine(`Successfully created file at: ${args.target_file}`);
72
80
  return {
73
81
  content: [{ type: 'text', text: 'ok' }],
74
82
  };
75
83
  } catch (error) {
76
84
  logger.appendLine(`Error during file creation: ${error}`);
77
85
  return {
78
- content: [{ type: 'text', text: 'unknown error' }],
86
+ content: [{ type: 'text', text: error.message }],
79
87
  isError: true,
80
88
  };
81
89
  }
@@ -14,7 +14,7 @@ export class EditFileHandler {
14
14
 
15
15
  async handler(params: { targetFile: string; codeEdit: string; instructions?: string }, toolCallId: string) {
16
16
  const { targetFile, codeEdit } = params;
17
- const block = this.applyService.registerCodeBlock(targetFile, codeEdit, toolCallId);
17
+ const block = await this.applyService.registerCodeBlock(targetFile, codeEdit, toolCallId);
18
18
  const blockData = await this.applyService.apply(block);
19
19
  return blockData;
20
20
  }