@opensumi/ide-ai-native 3.8.2-next-1741418699.0 → 3.8.2

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 (112) hide show
  1. package/lib/browser/ai-core.contribution.d.ts.map +1 -1
  2. package/lib/browser/ai-core.contribution.js +20 -0
  3. package/lib/browser/ai-core.contribution.js.map +1 -1
  4. package/lib/browser/chat/apply.service.js +1 -1
  5. package/lib/browser/chat/apply.service.js.map +1 -1
  6. package/lib/browser/chat/chat-manager.service.d.ts +2 -1
  7. package/lib/browser/chat/chat-manager.service.d.ts.map +1 -1
  8. package/lib/browser/chat/chat-manager.service.js +26 -7
  9. package/lib/browser/chat/chat-manager.service.js.map +1 -1
  10. package/lib/browser/chat/chat-model.d.ts +3 -3
  11. package/lib/browser/chat/chat-model.d.ts.map +1 -1
  12. package/lib/browser/chat/chat-model.js +22 -9
  13. package/lib/browser/chat/chat-model.js.map +1 -1
  14. package/lib/browser/chat/chat-proxy.service.d.ts +1 -0
  15. package/lib/browser/chat/chat-proxy.service.d.ts.map +1 -1
  16. package/lib/browser/chat/chat-proxy.service.js +2 -0
  17. package/lib/browser/chat/chat-proxy.service.js.map +1 -1
  18. package/lib/browser/chat/chat.view.d.ts.map +1 -1
  19. package/lib/browser/chat/chat.view.js +59 -2
  20. package/lib/browser/chat/chat.view.js.map +1 -1
  21. package/lib/browser/components/ApplyStatus.d.ts +7 -0
  22. package/lib/browser/components/ApplyStatus.d.ts.map +1 -0
  23. package/lib/browser/components/ApplyStatus.js +32 -0
  24. package/lib/browser/components/ApplyStatus.js.map +1 -0
  25. package/lib/browser/components/ChangeList.d.ts +17 -0
  26. package/lib/browser/components/ChangeList.d.ts.map +1 -0
  27. package/lib/browser/components/ChangeList.js +72 -0
  28. package/lib/browser/components/ChangeList.js.map +1 -0
  29. package/lib/browser/components/change-list.module.less +126 -0
  30. package/lib/browser/components/chat-history.module.less +1 -1
  31. package/lib/browser/components/components.module.less +14 -0
  32. package/lib/browser/index.d.ts.map +1 -1
  33. package/lib/browser/index.js +4 -4
  34. package/lib/browser/index.js.map +1 -1
  35. package/lib/browser/mcp/base-apply.service.d.ts +15 -7
  36. package/lib/browser/mcp/base-apply.service.d.ts.map +1 -1
  37. package/lib/browser/mcp/base-apply.service.js +84 -54
  38. package/lib/browser/mcp/base-apply.service.js.map +1 -1
  39. package/lib/browser/mcp/config/components/mcp-config.view.d.ts.map +1 -1
  40. package/lib/browser/mcp/config/components/mcp-config.view.js +18 -28
  41. package/lib/browser/mcp/config/components/mcp-config.view.js.map +1 -1
  42. package/lib/browser/mcp/config/components/mcp-server-form.d.ts.map +1 -1
  43. package/lib/browser/mcp/config/components/mcp-server-form.js +25 -33
  44. package/lib/browser/mcp/config/components/mcp-server-form.js.map +1 -1
  45. package/lib/browser/mcp/tools/components/EditFile.js +3 -24
  46. package/lib/browser/mcp/tools/components/EditFile.js.map +1 -1
  47. package/lib/browser/mcp/tools/components/ExpandableFileList.js +3 -2
  48. package/lib/browser/mcp/tools/components/ExpandableFileList.js.map +1 -1
  49. package/lib/browser/mcp/tools/handlers/EditFile.js +2 -2
  50. package/lib/browser/mcp/tools/handlers/EditFile.js.map +1 -1
  51. package/lib/browser/model/msg-history-manager.d.ts +0 -2
  52. package/lib/browser/model/msg-history-manager.d.ts.map +1 -1
  53. package/lib/browser/model/msg-history-manager.js +1 -6
  54. package/lib/browser/model/msg-history-manager.js.map +1 -1
  55. package/lib/browser/preferences/schema.d.ts.map +1 -1
  56. package/lib/browser/preferences/schema.js +8 -0
  57. package/lib/browser/preferences/schema.js.map +1 -1
  58. package/lib/browser/widget/inline-diff/inline-diff-manager.js +2 -2
  59. package/lib/browser/widget/inline-diff/inline-diff-manager.js.map +1 -1
  60. package/lib/browser/widget/inline-diff/inline-diff.controller.d.ts +1 -1
  61. package/lib/browser/widget/inline-diff/inline-diff.controller.d.ts.map +1 -1
  62. package/lib/browser/widget/inline-diff/inline-diff.controller.js.map +1 -1
  63. package/lib/browser/widget/inline-diff/inline-diff.service.d.ts +3 -2
  64. package/lib/browser/widget/inline-diff/inline-diff.service.d.ts.map +1 -1
  65. package/lib/browser/widget/inline-diff/inline-diff.service.js.map +1 -1
  66. package/lib/browser/widget/inline-stream-diff/inline-stream-diff.handler.d.ts +1 -1
  67. package/lib/browser/widget/inline-stream-diff/inline-stream-diff.handler.d.ts.map +1 -1
  68. package/lib/browser/widget/inline-stream-diff/inline-stream-diff.handler.js.map +1 -1
  69. package/lib/browser/widget/inline-stream-diff/live-preview.component.d.ts +0 -33
  70. package/lib/browser/widget/inline-stream-diff/live-preview.component.d.ts.map +1 -1
  71. package/lib/browser/widget/inline-stream-diff/live-preview.component.js +1 -6
  72. package/lib/browser/widget/inline-stream-diff/live-preview.component.js.map +1 -1
  73. package/lib/browser/widget/inline-stream-diff/live-preview.decoration.d.ts.map +1 -1
  74. package/lib/browser/widget/inline-stream-diff/live-preview.decoration.js +15 -14
  75. package/lib/browser/widget/inline-stream-diff/live-preview.decoration.js.map +1 -1
  76. package/lib/common/index.d.ts +7 -2
  77. package/lib/common/index.d.ts.map +1 -1
  78. package/lib/common/index.js +3 -2
  79. package/lib/common/index.js.map +1 -1
  80. package/lib/common/types.d.ts +33 -0
  81. package/lib/common/types.d.ts.map +1 -1
  82. package/lib/common/types.js +6 -1
  83. package/lib/common/types.js.map +1 -1
  84. package/package.json +23 -23
  85. package/src/browser/ai-core.contribution.ts +28 -0
  86. package/src/browser/chat/apply.service.ts +1 -1
  87. package/src/browser/chat/chat-manager.service.ts +54 -33
  88. package/src/browser/chat/chat-model.ts +22 -8
  89. package/src/browser/chat/chat-proxy.service.ts +2 -0
  90. package/src/browser/chat/chat.view.tsx +78 -4
  91. package/src/browser/components/ApplyStatus.tsx +44 -0
  92. package/src/browser/components/ChangeList.tsx +131 -0
  93. package/src/browser/components/change-list.module.less +126 -0
  94. package/src/browser/components/chat-history.module.less +1 -1
  95. package/src/browser/components/components.module.less +14 -0
  96. package/src/browser/index.ts +5 -4
  97. package/src/browser/mcp/base-apply.service.ts +93 -64
  98. package/src/browser/mcp/config/components/mcp-config.view.tsx +12 -23
  99. package/src/browser/mcp/config/components/mcp-server-form.tsx +54 -68
  100. package/src/browser/mcp/tools/components/EditFile.tsx +3 -37
  101. package/src/browser/mcp/tools/components/ExpandableFileList.tsx +3 -1
  102. package/src/browser/mcp/tools/handlers/EditFile.ts +2 -2
  103. package/src/browser/model/msg-history-manager.ts +2 -11
  104. package/src/browser/preferences/schema.ts +8 -0
  105. package/src/browser/widget/inline-diff/inline-diff-manager.tsx +2 -2
  106. package/src/browser/widget/inline-diff/inline-diff.controller.ts +2 -1
  107. package/src/browser/widget/inline-diff/inline-diff.service.ts +3 -2
  108. package/src/browser/widget/inline-stream-diff/inline-stream-diff.handler.tsx +4 -4
  109. package/src/browser/widget/inline-stream-diff/live-preview.component.tsx +0 -34
  110. package/src/browser/widget/inline-stream-diff/live-preview.decoration.tsx +8 -9
  111. package/src/common/index.ts +9 -2
  112. package/src/common/types.ts +35 -0
@@ -37,7 +37,7 @@ export class ApplyService extends BaseApplyService {
37
37
  <code>${fileContent}</code>
38
38
 
39
39
  <update>${codeBlock.codeEdit}</update>
40
-
40
+ ${codeBlock.instructions ? `\nUser's intention: ${codeBlock.instructions}\n` : ''}
41
41
  Provide the complete updated code.
42
42
  `,
43
43
  {
@@ -1,19 +1,21 @@
1
1
  import { Autowired, INJECTOR_TOKEN, Injectable, Injector } from '@opensumi/di';
2
+ import { PreferenceService } from '@opensumi/ide-core-browser';
2
3
  import {
4
+ AINativeSettingSectionsId,
3
5
  CancellationToken,
4
6
  CancellationTokenSource,
5
7
  Disposable,
6
8
  DisposableMap,
7
9
  Emitter,
8
10
  IChatProgress,
9
- IChatToolContent,
11
+ IDisposable,
10
12
  IStorage,
13
+ LRUCache,
11
14
  STORAGE_NAMESPACE,
12
15
  StorageProvider,
13
16
  debounce,
14
- formatLocalize,
15
17
  } from '@opensumi/ide-core-common';
16
- import { ChatMessageRole, IChatMessage, IHistoryChatMessage } from '@opensumi/ide-core-common/lib/types/ai-native';
18
+ import { IHistoryChatMessage } from '@opensumi/ide-core-common/lib/types/ai-native';
17
19
 
18
20
  import { IChatAgentService, IChatFollowup, IChatRequestMessage, IChatResponseErrorDetails } from '../../common';
19
21
  import { MsgHistoryManager } from '../model/msg-history-manager';
@@ -39,9 +41,26 @@ interface ISessionModel {
39
41
 
40
42
  const MAX_SESSION_COUNT = 20;
41
43
 
44
+ class DisposableLRUCache<K, V extends IDisposable = IDisposable> extends LRUCache<K, V> implements IDisposable {
45
+ disposeKey(key: K): void {
46
+ const disposable = this.get(key);
47
+ if (disposable) {
48
+ disposable.dispose();
49
+ }
50
+ this.delete(key);
51
+ }
52
+
53
+ dispose(): void {
54
+ this.forEach((disposable) => {
55
+ disposable.dispose();
56
+ });
57
+ this.clear();
58
+ }
59
+ }
60
+
42
61
  @Injectable()
43
62
  export class ChatManagerService extends Disposable {
44
- #sessionModels = this.registerDispose(new DisposableMap<string, ChatModel>());
63
+ #sessionModels = this.registerDispose(new DisposableLRUCache<string, ChatModel>(MAX_SESSION_COUNT));
45
64
  #pendingRequests = this.registerDispose(new DisposableMap<string, CancellationTokenSource>());
46
65
  private storageInitEmitter = new Emitter<void>();
47
66
  public onStorageInit = this.storageInitEmitter.event;
@@ -55,35 +74,39 @@ export class ChatManagerService extends Disposable {
55
74
  @Autowired(StorageProvider)
56
75
  private storageProvider: StorageProvider;
57
76
 
77
+ @Autowired(PreferenceService)
78
+ private preferenceService: PreferenceService;
79
+
58
80
  private _chatStorage: IStorage;
59
81
 
60
82
  protected fromJSON(data: ISessionModel[]) {
61
- // TODO: 支持ApplyService恢复
62
- return data.map((item) => {
63
- const model = new ChatModel({
64
- sessionId: item.sessionId,
65
- history: new MsgHistoryManager(item.history),
83
+ return data
84
+ .filter((item) => item.history.messages.length > 0)
85
+ .map((item) => {
86
+ const model = new ChatModel({
87
+ sessionId: item.sessionId,
88
+ history: new MsgHistoryManager(item.history),
89
+ });
90
+ const requests = item.requests.map(
91
+ (request) =>
92
+ new ChatRequestModel(
93
+ request.requestId,
94
+ model,
95
+ request.message,
96
+ new ChatResponseModel(request.requestId, model, request.message.agentId, {
97
+ responseContents: request.response.responseContents,
98
+ isComplete: true,
99
+ responseText: request.response.responseText,
100
+ responseParts: request.response.responseParts,
101
+ errorDetails: request.response.errorDetails,
102
+ followups: request.response.followups,
103
+ isCanceled: request.response.isCanceled,
104
+ }),
105
+ ),
106
+ );
107
+ model.restoreRequests(requests);
108
+ return model;
66
109
  });
67
- const requests = item.requests.map(
68
- (request) =>
69
- new ChatRequestModel(
70
- request.requestId,
71
- model,
72
- request.message,
73
- new ChatResponseModel(request.requestId, model, request.message.agentId, {
74
- responseContents: request.response.responseContents,
75
- isComplete: true,
76
- responseText: request.response.responseText,
77
- responseParts: request.response.responseParts,
78
- errorDetails: request.response.errorDetails,
79
- followups: request.response.followups,
80
- isCanceled: request.response.isCanceled,
81
- }),
82
- ),
83
- );
84
- model.restoreRequests(requests);
85
- return model;
86
- });
87
110
  }
88
111
 
89
112
  constructor() {
@@ -106,9 +129,6 @@ export class ChatManagerService extends Disposable {
106
129
  }
107
130
 
108
131
  startSession() {
109
- if (this.#sessionModels.size >= MAX_SESSION_COUNT) {
110
- throw new Error(formatLocalize('aiNative.chat.session.max', MAX_SESSION_COUNT.toString()));
111
- }
112
132
  const model = new ChatModel();
113
133
  this.#sessionModels.set(model.sessionId, model);
114
134
  this.listenSession(model);
@@ -156,7 +176,8 @@ export class ChatManagerService extends Disposable {
156
176
  request.response.cancel();
157
177
  });
158
178
 
159
- const history = model.messageHistory;
179
+ const contextWindow = this.preferenceService.get<number>(AINativeSettingSectionsId.ContextWindow);
180
+ const history = model.getMessageHistory(contextWindow);
160
181
 
161
182
  try {
162
183
  const progressCallback = (progress: IChatProgress) => {
@@ -276,15 +276,12 @@ export class ChatRequestModel implements IChatRequestModel {
276
276
  }
277
277
 
278
278
  export class ChatModel extends Disposable implements IChatModel {
279
- private static requestIdPool = 0;
279
+ private requestIdPool = 0;
280
280
 
281
- constructor(initParams?: { sessionId?: string; history?: MsgHistoryManager; requests?: ChatRequestModel[] }) {
281
+ constructor(initParams?: { sessionId?: string; history?: MsgHistoryManager }) {
282
282
  super();
283
283
  this.#sessionId = initParams?.sessionId ?? uuid();
284
284
  this.history = initParams?.history ?? new MsgHistoryManager();
285
- if (initParams?.requests) {
286
- this.#requests = new Map(initParams.requests.map((r) => [r.requestId, r]));
287
- }
288
285
  }
289
286
 
290
287
  #sessionId: string;
@@ -299,11 +296,18 @@ export class ChatModel extends Disposable implements IChatModel {
299
296
 
300
297
  restoreRequests(requests: ChatRequestModel[]): void {
301
298
  this.#requests = new Map(requests.map((r) => [r.requestId, r]));
299
+ this.requestIdPool = requests.length;
302
300
  }
303
301
 
304
302
  readonly history: MsgHistoryManager;
305
303
 
306
- get messageHistory() {
304
+ #slicedMessageCount = 0;
305
+
306
+ public get slicedMessageCount() {
307
+ return this.#slicedMessageCount;
308
+ }
309
+
310
+ getMessageHistory(contextWindow?: number) {
307
311
  const history: CoreMessage[] = [];
308
312
  for (const request of this.requests) {
309
313
  if (!request.response.isComplete) {
@@ -352,13 +356,23 @@ export class ChatModel extends Disposable implements IChatModel {
352
356
  }
353
357
  }
354
358
  }
355
- return history;
359
+ if (contextWindow) {
360
+ while (this.#slicedMessageCount < history.length) {
361
+ // 简单的使用 JSON.stringify 计算 token 数量
362
+ const tokenCount = JSON.stringify(history.slice(this.#slicedMessageCount)).length / 3;
363
+ if (tokenCount <= contextWindow) {
364
+ break;
365
+ }
366
+ this.#slicedMessageCount++;
367
+ }
368
+ }
369
+ return history.slice(this.#slicedMessageCount);
356
370
  }
357
371
 
358
372
  addRequest(message: IChatRequestMessage): ChatRequestModel {
359
373
  const msg = message;
360
374
 
361
- const requestId = `${this.sessionId}_request_${ChatModel.requestIdPool++}`;
375
+ const requestId = `${this.sessionId}_request_${this.requestIdPool++}`;
362
376
  const response = new ChatResponseModel(requestId, this, msg.agentId);
363
377
  const request = new ChatRequestModel(requestId, this, msg, response);
364
378
 
@@ -92,6 +92,7 @@ export class ChatProxyService extends Disposable {
92
92
  apiKey = this.preferenceService.get<string>(AINativeSettingSectionsId.OpenaiApiKey, '');
93
93
  baseURL = this.preferenceService.get<string>(AINativeSettingSectionsId.OpenaiBaseURL, '');
94
94
  }
95
+ const maxTokens = this.preferenceService.get<number>(AINativeSettingSectionsId.MaxTokens);
95
96
  const agent = this.chatAgentService.getAgent(ChatProxyService.AGENT_ID);
96
97
  return {
97
98
  clientId: this.applicationService.clientId,
@@ -99,6 +100,7 @@ export class ChatProxyService extends Disposable {
99
100
  modelId,
100
101
  apiKey,
101
102
  baseURL,
103
+ maxTokens,
102
104
  system: agent?.metadata.systemPrompt,
103
105
  };
104
106
  }
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { MessageList } from 'react-chat-elements';
3
3
 
4
- import { getIcon, useInjectable, useUpdateOnEvent } from '@opensumi/ide-core-browser';
4
+ import { AppConfig, getIcon, useInjectable, useUpdateOnEvent } from '@opensumi/ide-core-browser';
5
5
  import { Popover, PopoverPosition } from '@opensumi/ide-core-browser/lib/components';
6
6
  import { EnhanceIcon } from '@opensumi/ide-core-browser/lib/components/ai-native';
7
7
  import {
@@ -19,15 +19,20 @@ import {
19
19
  IAIReporter,
20
20
  IChatComponent,
21
21
  IChatContent,
22
+ URI,
22
23
  formatLocalize,
23
24
  localize,
25
+ path,
24
26
  uuid,
25
27
  } from '@opensumi/ide-core-common';
28
+ import { WorkbenchEditorService } from '@opensumi/ide-editor';
26
29
  import { IMainLayoutService } from '@opensumi/ide-main-layout';
27
30
  import { IMessageService } from '@opensumi/ide-overlay';
28
31
 
29
32
  import 'react-chat-elements/dist/main.css';
30
33
  import { AI_CHAT_VIEW_ID, IChatAgentService, IChatInternalService, IChatMessageStructure } from '../../common';
34
+ import { CodeBlockData } from '../../common/types';
35
+ import { FileChange, FileListDisplay } from '../components/ChangeList';
31
36
  import { ChatContext } from '../components/ChatContext';
32
37
  import { CodeBlockWrapperInput } from '../components/ChatEditor';
33
38
  import ChatHistory, { IChatHistoryItem } from '../components/ChatHistory';
@@ -37,6 +42,7 @@ import { ChatNotify, ChatReply } from '../components/ChatReply';
37
42
  import { SlashCustomRender } from '../components/SlashCustomRender';
38
43
  import { MessageData, createMessageByAI, createMessageByUser } from '../components/utils';
39
44
  import { WelcomeMessage } from '../components/WelcomeMsg';
45
+ import { BaseApplyService } from '../mcp/base-apply.service';
40
46
  import { ChatViewHeaderRender, IMCPServerRegistry, TSlashCommandCustomRender, TokenMCPServerRegistry } from '../types';
41
47
 
42
48
  import { ChatRequestModel, ChatSlashCommandItemModel } from './chat-model';
@@ -56,6 +62,41 @@ interface TDispatchAction {
56
62
 
57
63
  const MAX_TITLE_LENGTH = 100;
58
64
 
65
+ const getFileChanges = (codeBlocks: CodeBlockData[]) =>
66
+ codeBlocks
67
+ .map((block) => {
68
+ const rangesFromDiffHunk =
69
+ block.applyResult?.diff.split('\n').reduce(
70
+ ([del, add], line) => {
71
+ if (line.startsWith('-')) {
72
+ del += 1;
73
+ } else if (line.startsWith('+')) {
74
+ add += 1;
75
+ }
76
+ return [del, add];
77
+ },
78
+ [0, 0],
79
+ ) || [];
80
+ return {
81
+ path: block.relativePath,
82
+ additions: rangesFromDiffHunk[1],
83
+ deletions: rangesFromDiffHunk[0],
84
+ status: block.status,
85
+ };
86
+ })
87
+ .reduce((acc, curr) => {
88
+ const existingFile = acc.find((file) => file.path === curr.path);
89
+ if (existingFile) {
90
+ existingFile.additions += curr.additions;
91
+ existingFile.deletions += curr.deletions;
92
+ // 使用最新的状态
93
+ existingFile.status = curr.status;
94
+ } else {
95
+ acc.push(curr);
96
+ }
97
+ return acc;
98
+ }, [] as FileChange[]);
99
+
59
100
  export const AIChatView = () => {
60
101
  const aiChatService = useInjectable<ChatInternalService>(IChatInternalService);
61
102
  const chatApiService = useInjectable<ChatService>(ChatServiceToken);
@@ -70,9 +111,13 @@ export const AIChatView = () => {
70
111
  const containerRef = React.useRef<HTMLDivElement>(null);
71
112
  const autoScroll = React.useRef<boolean>(true);
72
113
  const chatInputRef = React.useRef<{ setInputValue: (v: string) => void } | null>(null);
73
-
114
+ const editorService = useInjectable<WorkbenchEditorService>(WorkbenchEditorService);
115
+ const appConfig = useInjectable<AppConfig>(AppConfig);
116
+ const applyService = useInjectable<BaseApplyService>(BaseApplyService);
74
117
  const [shortcutCommands, setShortcutCommands] = React.useState<ChatSlashCommandItemModel[]>([]);
75
118
 
119
+ const [changeList, setChangeList] = React.useState<FileChange[]>(getFileChanges(applyService.getSessionCodeBlocks()));
120
+
76
121
  const [messageListData, dispatchMessage] = React.useReducer((state: MessageData[], action: TDispatchAction) => {
77
122
  switch (action.type) {
78
123
  case 'add':
@@ -92,6 +137,18 @@ export const AIChatView = () => {
92
137
  const [command, setCommand] = React.useState('');
93
138
  const [theme, setTheme] = React.useState<string | null>(null);
94
139
 
140
+ React.useEffect(() => {
141
+ const disposer = new Disposable();
142
+ const doUpdate = () => {
143
+ const fileChanges = getFileChanges(applyService.getSessionCodeBlocks());
144
+ setChangeList(fileChanges);
145
+ };
146
+ disposer.addDispose(aiChatService.onChangeSession(doUpdate));
147
+ // TODO: 全量获取性能不好
148
+ disposer.addDispose(applyService.onCodeBlockUpdate(doUpdate));
149
+ return () => disposer.dispose();
150
+ }, []);
151
+
95
152
  React.useEffect(() => {
96
153
  const featureSlashCommands = chatFeatureRegistry.getAllShortcutSlashCommand();
97
154
 
@@ -691,10 +748,13 @@ export const AIChatView = () => {
691
748
  dataSource={messageListData}
692
749
  />
693
750
  </div>
694
- {msgHistoryManager.slicedMessageCount ? (
751
+ {aiChatService.sessionModel.slicedMessageCount ? (
695
752
  <div className={styles.chat_tips_text}>
696
753
  <div className={styles.chat_tips_container}>
697
- {formatLocalize('aiNative.chat.ai.assistant.limit.message', msgHistoryManager.slicedMessageCount)}
754
+ {formatLocalize(
755
+ 'aiNative.chat.ai.assistant.limit.message',
756
+ aiChatService.sessionModel.slicedMessageCount,
757
+ )}
698
758
  </div>
699
759
  </div>
700
760
  ) : null}
@@ -715,6 +775,20 @@ export const AIChatView = () => {
715
775
  ))}
716
776
  </div>
717
777
  </div>
778
+ {changeList.length > 0 && (
779
+ <FileListDisplay
780
+ files={changeList}
781
+ onFileClick={(filePath) => {
782
+ editorService.open(URI.file(path.join(appConfig.workspaceDir, filePath)));
783
+ }}
784
+ onRejectAll={() => {
785
+ applyService.processAll('reject');
786
+ }}
787
+ onAcceptAll={() => {
788
+ applyService.processAll('accept');
789
+ }}
790
+ />
791
+ )}
718
792
  <ChatInputWrapperRender
719
793
  onSend={(value, agentId, command) =>
720
794
  handleSend({
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+
3
+ import { Icon, Loading, Popover } from '@opensumi/ide-components';
4
+
5
+ import { CodeBlockStatus } from '../../common/types';
6
+
7
+ import styles from './components.module.less';
8
+
9
+ export const ApplyStatus = ({ status, error }: { status: CodeBlockStatus; error?: string }) => {
10
+ const renderStatus = () => {
11
+ switch (status) {
12
+ case 'generating':
13
+ return <Loading />;
14
+ case 'pending':
15
+ return (
16
+ <Popover title='Pending' id={'edit-file-tool-status-pending'}>
17
+ <Icon iconClass='codicon codicon-circle-large' />
18
+ </Popover>
19
+ );
20
+ case 'success':
21
+ return (
22
+ <Popover title='Success' id={'edit-file-tool-status-success'}>
23
+ <Icon iconClass='codicon codicon-pass' />
24
+ </Popover>
25
+ );
26
+ case 'failed':
27
+ return (
28
+ <Popover title={`Failed (${error || 'Unknown error'})`} id={'edit-file-tool-status-failed'}>
29
+ <Icon iconClass='codicon codicon-error' />
30
+ </Popover>
31
+ );
32
+ case 'cancelled':
33
+ return (
34
+ <Popover title='Cancelled' id={'edit-file-tool-status-cancelled'}>
35
+ <Icon iconClass='codicon codicon-circle-slash' style={{ color: 'var(--input-placeholderForeground)' }} />
36
+ </Popover>
37
+ );
38
+ default:
39
+ return null;
40
+ }
41
+ };
42
+
43
+ return <span className={styles.status}>{renderStatus()}</span>;
44
+ };
@@ -0,0 +1,131 @@
1
+ import React, { useMemo, useState } from 'react';
2
+
3
+ import { Button, Icon } from '@opensumi/ide-components';
4
+ import { LabelService, URI, useInjectable } from '@opensumi/ide-core-browser';
5
+
6
+ import { CodeBlockStatus } from '../../common/types';
7
+
8
+ import { ApplyStatus } from './ApplyStatus';
9
+ import styles from './change-list.module.less';
10
+
11
+ export interface FileChange {
12
+ path: string;
13
+ additions: number;
14
+ deletions: number;
15
+ status: CodeBlockStatus;
16
+ }
17
+
18
+ interface FileListDisplayProps {
19
+ files: FileChange[];
20
+ onFileClick: (path: string) => void;
21
+ onRejectAll: () => void;
22
+ onAcceptAll: () => void;
23
+ }
24
+
25
+ export const FileListDisplay: React.FC<FileListDisplayProps> = (props) => {
26
+ const { files, onFileClick, onRejectAll, onAcceptAll } = props;
27
+ const [isExpanded, setIsExpanded] = useState(true);
28
+ const labelService = useInjectable<LabelService>(LabelService);
29
+ const fileIcons = useMemo(
30
+ () =>
31
+ files.map((file) => {
32
+ const uri = URI.parse(file.path);
33
+ const iconClass = labelService.getIcon(uri);
34
+ return <span className={iconClass}></span>;
35
+ }),
36
+ [files],
37
+ );
38
+
39
+ const totalFiles = files.length;
40
+ const totalChanges = files.reduce(
41
+ (acc, file) => ({
42
+ additions: acc.additions + file.additions,
43
+ deletions: acc.deletions + file.deletions,
44
+ }),
45
+ { additions: 0, deletions: 0 },
46
+ );
47
+
48
+ const handleToggle = () => {
49
+ setIsExpanded(!isExpanded);
50
+ };
51
+
52
+ const renderChangeStats = (additions: number, deletions: number) => {
53
+ const parts: React.ReactNode[] = [];
54
+
55
+ if (additions > 0) {
56
+ parts.push(
57
+ <span key='add' className={styles.additions}>
58
+ +{additions}
59
+ </span>,
60
+ );
61
+ }
62
+
63
+ if (deletions > 0) {
64
+ if (parts.length > 0) {
65
+ parts.push(' ');
66
+ }
67
+ parts.push(
68
+ <span key='del' className={styles.deletions}>
69
+ -{deletions}
70
+ </span>,
71
+ );
72
+ }
73
+
74
+ if (parts.length === 0) {
75
+ parts.push(
76
+ <span key='no-change' className={styles.noChange}>
77
+ No changes
78
+ </span>,
79
+ );
80
+ }
81
+
82
+ return parts;
83
+ };
84
+
85
+ return (
86
+ <div className={styles.container}>
87
+ <div className={styles.header} onClick={handleToggle}>
88
+ <span className={styles.title}>
89
+ <button className={styles.toggleButton}>{isExpanded ? <Icon icon='down' /> : <Icon icon='right' />}</button>
90
+ Changed Files ({totalFiles}) {renderChangeStats(totalChanges.additions, totalChanges.deletions)}
91
+ </span>
92
+ {files.some((file) => file.status === 'pending') && (
93
+ <div className={styles.actions}>
94
+ <Button
95
+ type='link'
96
+ size='small'
97
+ onClick={(e) => {
98
+ e.stopPropagation();
99
+ onRejectAll();
100
+ }}
101
+ >
102
+ Reject
103
+ </Button>
104
+ <Button
105
+ size='small'
106
+ onClick={(e) => {
107
+ e.stopPropagation();
108
+ onAcceptAll();
109
+ }}
110
+ >
111
+ Accept
112
+ </Button>
113
+ </div>
114
+ )}
115
+ </div>
116
+
117
+ <ul className={`${styles.fileList} ${!isExpanded ? styles.collapsed : ''}`}>
118
+ {files.map((file, index) => (
119
+ <li key={index} className={styles.fileItem} onClick={() => onFileClick(file.path)}>
120
+ <span className={styles.fileIcon}>{fileIcons[index] || '📄'}</span>
121
+ <div className={styles.fileInfo}>
122
+ <div className={styles.filePath}>{file.path}</div>
123
+ <div className={styles.fileStats}>{renderChangeStats(file.additions, file.deletions)}</div>
124
+ <ApplyStatus status={file.status} />
125
+ </div>
126
+ </li>
127
+ ))}
128
+ </ul>
129
+ </div>
130
+ );
131
+ };
@@ -0,0 +1,126 @@
1
+ .container {
2
+ margin: 0 12px;
3
+ background: var(--design-chatInput-background);
4
+ border-radius: 9px 9px 0 0;
5
+ border: 1px solid var(--kt-input-border);
6
+ border-bottom: none;
7
+ overflow: hidden;
8
+ padding-bottom: 6px;
9
+ }
10
+
11
+ .header {
12
+ display: flex;
13
+ align-items: center;
14
+ justify-content: space-between;
15
+ padding: 8px 12px 2px 12px;
16
+ background-color: transparent;
17
+ cursor: pointer;
18
+ user-select: none;
19
+ font-size: 12px;
20
+ color: var(--descriptionForeground);
21
+ }
22
+
23
+ .title:hover {
24
+ color: var(--design-text-highlightForeground);
25
+ .toggleButton :global(.kt-icon) {
26
+ color: var(--design-text-highlightForeground);
27
+ transition: color 0.2s ease-in-out;
28
+ }
29
+ }
30
+
31
+ .toggleButton {
32
+ background: none;
33
+ border: none;
34
+ cursor: pointer;
35
+ padding: 0;
36
+ width: 16px;
37
+ height: 16px;
38
+ display: flex;
39
+ align-items: center;
40
+ justify-content: center;
41
+ font-size: 12px;
42
+ }
43
+
44
+ .title {
45
+ gap: 6px;
46
+ transition: color 0.2s ease-in-out;
47
+ display: flex;
48
+ }
49
+
50
+ .fileList {
51
+ list-style: none;
52
+ margin: 0;
53
+ padding: 0 12px;
54
+ }
55
+
56
+ .fileItem {
57
+ display: flex;
58
+ align-items: center;
59
+ padding: 4px 0;
60
+ cursor: pointer;
61
+ border: none;
62
+ font-size: 13px;
63
+ color: var(--descriptionForeground);
64
+ }
65
+
66
+ .fileItem:hover {
67
+ background-color: var(--design-block-hoverBackground);
68
+ }
69
+
70
+ .fileIcon {
71
+ margin-right: 6px;
72
+ width: 16px;
73
+ height: 16px;
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ }
78
+
79
+ .fileInfo {
80
+ flex: 1;
81
+ display: flex;
82
+ justify-content: space-between;
83
+ align-items: center;
84
+ overflow: hidden;
85
+ :global(.kt-popover-trigger) {
86
+ margin-left: 8px;
87
+ }
88
+ :global(.codicon) {
89
+ font-size: 12px !important;
90
+ position: relative;
91
+ top: 1px;
92
+ }
93
+ }
94
+
95
+ .filePath {
96
+ font-size: 12px;
97
+ margin: 0;
98
+ white-space: nowrap;
99
+ overflow: hidden;
100
+ text-overflow: ellipsis;
101
+ }
102
+
103
+ .fileStats {
104
+ font-size: 12px;
105
+ display: flex;
106
+ align-items: center;
107
+ gap: 4px;
108
+ }
109
+
110
+ .additions {
111
+ color: #52c41a;
112
+ }
113
+
114
+ .deletions {
115
+ color: var(--debugConsole-errorForeground);
116
+ }
117
+
118
+ .noChange {
119
+ color: #6a737d;
120
+ font-style: italic;
121
+ white-space: nowrap;
122
+ }
123
+
124
+ .collapsed {
125
+ display: none;
126
+ }
@@ -78,7 +78,7 @@
78
78
  .dm-chat-history-list {
79
79
  overflow: auto;
80
80
  max-height: 400px;
81
- max-width: 400px;
81
+ width: 300px;
82
82
  margin-top: 4px;
83
83
  font-size: 13px;
84
84
  }