Skip to content

Commit 82ddf9a

Browse files
authored
feat: support new chat mention input for MCP model (#4466)
* feat: support more powerfull input * chore: update iconfont * chore: update iconfont resource * style: support better style * chore: update icons * chore: update iconfont * chore: update css source * chore: update kaitian-icon resource * style: improve mention tag style * chore: update iconfont * feat: support more beautiful chat input * feat: support mention input placeholder * chore: update placeholder * style: improve empty search style * chore: update iconfont resource * chore: update iconfont resource * feat: support chat mention input UX * feat: support chat file and folder context * chore: add minWidth for chat selection * feat: add onSelectionChange prop to MentionInput component * feat: improve prompt * feat: support file and folder reference display * feat: support file and folder navigator * style: add global icon margin style in components * style: improve Chat UI * chore: remove useless code * chore: update styles * feat: improve folder location on file explorer * feat: optimize file tree explorer activation handling * chore: update mention input style * fix: folder and history style * style: improve chat history style * style: improve markdown style * fix: reval file or folder on explorer * refactor: improve file match logic
1 parent b1eeb00 commit 82ddf9a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2487
-183
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"build:all": "yarn build:webview-prebuilt && yarn run build && yarn run build:worker-host && yarn run build:ext-host && yarn build:watcher-host && yarn run build:components && yarn build:monaco-worker",
2020
"build:cli-engine": "cd tools/cli-engine && yarn run build",
2121
"build:components": "cd packages/components && yarn run build:dist",
22+
"build:components:lib": "cd packages/components && yarn run build",
2223
"build:ext-host": "cd packages/extension && yarn run build:ext-host",
2324
"build:watcher-host": "cd packages/file-service && yarn run build:watcher-host",
2425
"build:monaco-worker": "cd packages/monaco && yarn run build:worker",

packages/ai-native/__test__/browser/chat/chat-agent.service.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe('ChatAgentService', () => {
2525
{
2626
token: ChatAgentPromptProvider,
2727
useValue: {
28-
provideContextPrompt: (val, msg) => msg,
28+
provideContextPrompt: async (val, msg) => msg,
2929
},
3030
},
3131
{

packages/ai-native/src/browser/chat/chat-agent.service.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
5959
private readonly aiReporter: IAIReporter;
6060

6161
@Autowired(LLMContextServiceToken)
62-
protected readonly contextService: LLMContextService;
62+
protected readonly llmContextService: LLMContextService;
6363

6464
@Autowired(ChatAgentPromptProvider)
6565
protected readonly promptProvider: ChatAgentPromptProvider;
@@ -74,7 +74,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
7474
super();
7575
this.addDispose(this._onDidChangeAgents);
7676
this.addDispose(
77-
this.contextService.onDidContextFilesChangeEvent((event) => {
77+
this.llmContextService.onDidContextFilesChangeEvent((event) => {
7878
if (event.version !== this.contextVersion) {
7979
this.contextVersion = event.version;
8080
this.shouldUpdateContext = true;
@@ -152,19 +152,19 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
152152
if (!this.initialUserMessageMap.has(request.sessionId)) {
153153
this.initialUserMessageMap.set(request.sessionId, request.message);
154154
const rawMessage = request.message;
155-
request.message = this.provideContextMessage(rawMessage, request.sessionId);
155+
request.message = await this.provideContextMessage(rawMessage, request.sessionId);
156156
} else if (this.shouldUpdateContext || request.regenerate || history.length === 0) {
157-
request.message = this.provideContextMessage(request.message, request.sessionId);
157+
request.message = await this.provideContextMessage(request.message, request.sessionId);
158158
this.shouldUpdateContext = false;
159159
}
160160

161161
const result = await data.agent.invoke(request, progress, history, token);
162162
return result;
163163
}
164164

165-
private provideContextMessage(message: string, sessionId: string) {
166-
const context = this.contextService.serialize();
167-
const fullMessage = this.promptProvider.provideContextPrompt(context, message);
165+
private async provideContextMessage(message: string, sessionId: string) {
166+
const context = await this.llmContextService.serialize();
167+
const fullMessage = await this.promptProvider.provideContextPrompt(context, message);
168168
this.aiReporter.send({
169169
msgType: AIServiceType.Chat,
170170
actionType: ActionTypeEnum.ContextEnhance,

packages/ai-native/src/browser/chat/chat.view.tsx

+81-24
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import * as React from 'react';
22
import { MessageList } from 'react-chat-elements';
33

4-
import { AppConfig, getIcon, useInjectable, useUpdateOnEvent } from '@opensumi/ide-core-browser';
4+
import {
5+
AINativeConfigService,
6+
AppConfig,
7+
LabelService,
8+
getIcon,
9+
useInjectable,
10+
useUpdateOnEvent,
11+
} from '@opensumi/ide-core-browser';
512
import { Popover, PopoverPosition } from '@opensumi/ide-core-browser/lib/components';
613
import { EnhanceIcon } from '@opensumi/ide-core-browser/lib/components/ai-native';
714
import {
@@ -14,6 +21,7 @@ import {
1421
ChatMessageRole,
1522
ChatRenderRegistryToken,
1623
ChatServiceToken,
24+
CommandService,
1725
Disposable,
1826
DisposableCollection,
1927
IAIReporter,
@@ -28,16 +36,19 @@ import {
2836
import { WorkbenchEditorService } from '@opensumi/ide-editor';
2937
import { IMainLayoutService } from '@opensumi/ide-main-layout';
3038
import { IMessageService } from '@opensumi/ide-overlay';
31-
3239
import 'react-chat-elements/dist/main.css';
40+
import { IWorkspaceService } from '@opensumi/ide-workspace';
41+
3342
import { AI_CHAT_VIEW_ID, IChatAgentService, IChatInternalService, IChatMessageStructure } from '../../common';
43+
import { LLMContextService, LLMContextServiceToken } from '../../common/llm-context';
3444
import { CodeBlockData } from '../../common/types';
45+
import { cleanAttachedTextWrapper } from '../../common/utils';
3546
import { FileChange, FileListDisplay } from '../components/ChangeList';
36-
import { ChatContext } from '../components/ChatContext';
3747
import { CodeBlockWrapperInput } from '../components/ChatEditor';
3848
import ChatHistory, { IChatHistoryItem } from '../components/ChatHistory';
3949
import { ChatInput } from '../components/ChatInput';
4050
import { ChatMarkdown } from '../components/ChatMarkdown';
51+
import { ChatMentionInput } from '../components/ChatMentionInput';
4152
import { ChatNotify, ChatReply } from '../components/ChatReply';
4253
import { SlashCustomRender } from '../components/SlashCustomRender';
4354
import { MessageData, createMessageByAI, createMessageByUser } from '../components/utils';
@@ -105,6 +116,8 @@ export const AIChatView = () => {
105116
const chatFeatureRegistry = useInjectable<ChatFeatureRegistry>(ChatFeatureRegistryToken);
106117
const chatRenderRegistry = useInjectable<ChatRenderRegistry>(ChatRenderRegistryToken);
107118
const mcpServerRegistry = useInjectable<IMCPServerRegistry>(TokenMCPServerRegistry);
119+
const aiNativeConfigService = useInjectable<AINativeConfigService>(AINativeConfigService);
120+
const llmContextService = useInjectable<LLMContextService>(LLMContextServiceToken);
108121

109122
const layoutService = useInjectable<IMainLayoutService>(IMainLayoutService);
110123
const msgHistoryManager = aiChatService.sessionModel.history;
@@ -114,6 +127,9 @@ export const AIChatView = () => {
114127
const editorService = useInjectable<WorkbenchEditorService>(WorkbenchEditorService);
115128
const appConfig = useInjectable<AppConfig>(AppConfig);
116129
const applyService = useInjectable<BaseApplyService>(BaseApplyService);
130+
const labelService = useInjectable<LabelService>(LabelService);
131+
const workspaceService = useInjectable<IWorkspaceService>(IWorkspaceService);
132+
const commandService = useInjectable<CommandService>(CommandService);
117133
const [shortcutCommands, setShortcutCommands] = React.useState<ChatSlashCommandItemModel[]>([]);
118134

119135
const [changeList, setChangeList] = React.useState<FileChange[]>(getFileChanges(applyService.getSessionCodeBlocks()));
@@ -184,6 +200,9 @@ export const AIChatView = () => {
184200
if (chatRenderRegistry.chatInputRender) {
185201
return chatRenderRegistry.chatInputRender;
186202
}
203+
if (aiNativeConfigService.capabilities.supportsMCP) {
204+
return ChatMentionInput;
205+
}
187206
return ChatInput;
188207
}, [chatRenderRegistry.chatInputRender]);
189208

@@ -262,7 +281,7 @@ export const AIChatView = () => {
262281
if (loading) {
263282
return;
264283
}
265-
await handleSend(message);
284+
await handleSend(message.message, message.agentId, message.command);
266285
} else {
267286
if (message.agentId) {
268287
setAgentId(message.agentId);
@@ -349,6 +368,9 @@ export const AIChatView = () => {
349368
text={message}
350369
agentId={visibleAgentId}
351370
command={command}
371+
labelService={labelService}
372+
workspaceService={workspaceService}
373+
commandService={commandService}
352374
/>
353375
),
354376
},
@@ -454,7 +476,15 @@ export const AIChatView = () => {
454476
text: ChatUserRoleRender ? (
455477
<ChatUserRoleRender content={message} agentId={visibleAgentId} command={command} />
456478
) : (
457-
<CodeBlockWrapperInput relationId={relationId} text={message} agentId={visibleAgentId} command={command} />
479+
<CodeBlockWrapperInput
480+
labelService={labelService}
481+
relationId={relationId}
482+
text={message}
483+
agentId={visibleAgentId}
484+
command={command}
485+
workspaceService={workspaceService}
486+
commandService={commandService}
487+
/>
458488
),
459489
},
460490
styles.chat_message_code,
@@ -634,15 +664,50 @@ export const AIChatView = () => {
634664
msgId,
635665
});
636666
},
637-
[chatRenderRegistry, chatRenderRegistry.chatUserRoleRender, msgHistoryManager, scrollToBottom],
667+
[chatRenderRegistry, chatRenderRegistry.chatUserRoleRender, msgHistoryManager, scrollToBottom, loading],
638668
);
639669

640670
const handleSend = React.useCallback(
641-
async (value: IChatMessageStructure) => {
642-
const { message, command, reportExtra } = value;
671+
async (message: string, agentId?: string, command?: string) => {
672+
const reportExtra = {
673+
actionSource: ActionSourceEnum.Chat,
674+
actionType: ActionTypeEnum.Send,
675+
};
676+
agentId = agentId ? agentId : ChatProxyService.AGENT_ID;
677+
// 提取并替换 {{@file:xxx}} 中的文件内容
678+
let processedContent = message;
679+
const filePattern = /\{\{@file:(.*?)\}\}/g;
680+
const fileMatches = message.match(filePattern);
681+
let isCleanContext = false;
682+
if (fileMatches) {
683+
for (const match of fileMatches) {
684+
const filePath = match.replace(/\{\{@file:(.*?)\}\}/, '$1');
685+
if (filePath && !isCleanContext) {
686+
isCleanContext = true;
687+
llmContextService.cleanFileContext();
688+
}
689+
const fileUri = new URI(filePath);
690+
llmContextService.addFileToContext(fileUri, undefined, true);
691+
const relativePath = (await workspaceService.asRelativePath(fileUri))?.path || fileUri.displayName;
692+
// 获取文件内容
693+
// 替换占位符,后续支持自定义渲染时可替换为自定义渲染标签
694+
processedContent = processedContent.replace(match, `\`<attached_file>${relativePath}\``);
695+
}
696+
}
643697

644-
const agentId = value.agentId ? value.agentId : ChatProxyService.AGENT_ID;
645-
return handleAgentReply({ message, agentId, command, reportExtra });
698+
const folderPattern = /\{\{@folder:(.*?)\}\}/g;
699+
const folderMatches = processedContent.match(folderPattern);
700+
if (folderMatches) {
701+
for (const match of folderMatches) {
702+
const folderPath = match.replace(/\{\{@folder:(.*?)\}\}/, '$1');
703+
const folderUri = new URI(folderPath);
704+
llmContextService.addFolderToContext(folderUri);
705+
const relativePath = (await workspaceService.asRelativePath(folderUri))?.path || folderUri.displayName;
706+
// 替换占位符,后续支持自定义渲染时可替换为自定义渲染标签
707+
processedContent = processedContent.replace(match, `\`<attached_folder>${relativePath}\``);
708+
}
709+
}
710+
return handleAgentReply({ message: processedContent, agentId, command, reportExtra });
646711
},
647712
[handleAgentReply],
648713
);
@@ -759,7 +824,6 @@ export const AIChatView = () => {
759824
</div>
760825
) : null}
761826
<div className={styles.chat_input_wrap}>
762-
<ChatContext />
763827
<div className={styles.header_operate}>
764828
<div className={styles.header_operate_left}>
765829
{shortcutCommands.map((command) => (
@@ -790,17 +854,7 @@ export const AIChatView = () => {
790854
/>
791855
)}
792856
<ChatInputWrapperRender
793-
onSend={(value, agentId, command) =>
794-
handleSend({
795-
message: value,
796-
agentId,
797-
command,
798-
reportExtra: {
799-
actionSource: ActionSourceEnum.Chat,
800-
actionType: ActionTypeEnum.Send,
801-
},
802-
})
803-
}
857+
onSend={handleSend}
804858
disabled={loading}
805859
enableOptions={true}
806860
theme={theme}
@@ -857,12 +911,15 @@ export function DefaultChatViewHeader({
857911
const getHistoryList = () => {
858912
const currentMessages = aiChatService.sessionModel.history.getMessages();
859913
const latestUserMessage = currentMessages.findLast((m) => m.role === ChatMessageRole.User);
860-
setCurrentTitle(latestUserMessage ? latestUserMessage.content.slice(0, MAX_TITLE_LENGTH) : '');
914+
setCurrentTitle(
915+
latestUserMessage ? cleanAttachedTextWrapper(latestUserMessage.content).slice(0, MAX_TITLE_LENGTH) : '',
916+
);
861917
setHistoryList(
862918
aiChatService.getSessions().map((session) => {
863919
const history = session.history;
864920
const messages = history.getMessages();
865-
const title = messages.length > 0 ? messages[0].content.slice(0, MAX_TITLE_LENGTH) : '';
921+
const title =
922+
messages.length > 0 ? cleanAttachedTextWrapper(messages[0].content).slice(0, MAX_TITLE_LENGTH) : '';
866923
const updatedAt = messages.length > 0 ? messages[messages.length - 1].replyStartTime || 0 : 0;
867924
// const loading = session.requests[session.requests.length - 1]?.response.isComplete;
868925
return {

0 commit comments

Comments
 (0)