Skip to content

Commit 05a43a6

Browse files
ensorrowlife2015Ricbet
authored
feat: support edit_file tool (#4385)
* feat: mcp server client poc * feat: introduce MCP tools contribution * fix: 修复 mcp sdk 引入类型问题 * feat: add builtin MCP server * fix: mcp types fix * fix: mcp types fix2 * feat: sumi mcp builtin sever * feat: code optimization * feat: support llm tool call streaming and ui, more mcp tools * feat: enhance language model error handling and streaming * feat: mcp tools grouped by clientId, add mcp tools panel * feat: add openai compatible api preferences * feat: support chat history in language model request * feat: add MCP server configuration support via preferences * feat: implement readfile & readdir tools * fix: tool impl bugs * refactor: use design system variables in ChatToolRender styles * refactor: improve logging and revert some unnecessary optimization * fix: logger not work in node.js * fix: mcp tool render fix * feat: add MCP and custom LLM config * fix: build error fix * fix: lint fix * fix: lint fix * fix: lint error fix * feat: format the tool call error message * feat: implement edit-file tool * feat: support system prompt & other config passthrough, fix apply * feat: implement apply demo with qwen-turbo * feat: implement edit_file tool view * feat: apply status * feat: cancel all * fix: dispose previewer when close * fix: simplify diff result * fix: adjust UI styling details in AI native components * fix: fix accept judgement logic * chore: simplify default chat system prompt * feat: support edit & diagnositc iteration * fix: edit tool display * fix: add key * feat: builtin tool support label * fix: cr * chore: validate * refactor: validate schema & transform args before run tool * fix: cr * feat: display instructions before apply * fix: deps & test * fix: deps * fix: missing deps --------- Co-authored-by: retrox.jcy <[email protected]> Co-authored-by: John <[email protected]>
1 parent 29f48cd commit 05a43a6

Some content is hidden

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

55 files changed

+1102
-196
lines changed

jest.setup.jsdom.js

+17-9
Original file line numberDiff line numberDiff line change
@@ -94,16 +94,24 @@ Object.defineProperty(window, 'matchMedia', {
9494
Object.defineProperty(window, 'crypto', {
9595
writable: true,
9696
value: {
97-
randomUUID: () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
98-
const r = Math.random() * 16 | 0;
99-
const v = c === 'x' ? r : (r & 0x3 | 0x8);
100-
return v.toString(16);
101-
}),
97+
randomUUID: () =>
98+
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
99+
const r = (Math.random() * 16) | 0;
100+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
101+
return v.toString(16);
102+
}),
102103
getRandomValues: (array) => {
103-
if (!(array instanceof Int8Array || array instanceof Uint8Array ||
104-
array instanceof Int16Array || array instanceof Uint16Array ||
105-
array instanceof Int32Array || array instanceof Uint32Array ||
106-
array instanceof Uint8ClampedArray)) {
104+
if (
105+
!(
106+
array instanceof Int8Array ||
107+
array instanceof Uint8Array ||
108+
array instanceof Int16Array ||
109+
array instanceof Uint16Array ||
110+
array instanceof Int32Array ||
111+
array instanceof Uint32Array ||
112+
array instanceof Uint8ClampedArray
113+
)
114+
) {
107115
throw new TypeError('Expected a TypedArray');
108116
}
109117
for (let i = 0; i < array.length; i++) {

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

+3
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ describe('ChatAgentService', () => {
5656
const agent = {
5757
id: 'agent1',
5858
invoke: jest.fn().mockResolvedValue({}),
59+
metadata: {
60+
systemPrompt: 'You are a helpful assistant.',
61+
},
5962
} as unknown as IChatAgent;
6063
chatAgentService.registerAgent(agent);
6164

packages/ai-native/package.json

+1-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
"@ai-sdk/anthropic": "^1.1.6",
2323
"@ai-sdk/deepseek": "^0.1.8",
2424
"@ai-sdk/openai": "^1.1.9",
25-
"@anthropic-ai/sdk": "^0.36.3",
2625
"@modelcontextprotocol/sdk": "^1.3.1",
2726
"@opensumi/ide-addons": "workspace:*",
2827
"@opensumi/ide-components": "workspace:*",
@@ -34,10 +33,8 @@
3433
"@opensumi/ide-editor": "workspace:*",
3534
"@opensumi/ide-file-search": "workspace:*",
3635
"@opensumi/ide-file-service": "workspace:*",
37-
"@opensumi/ide-file-tree-next": "workspace:*",
3836
"@opensumi/ide-main-layout": "workspace:*",
3937
"@opensumi/ide-markers": "workspace:*",
40-
"@opensumi/ide-menu-bar": "workspace:*",
4138
"@opensumi/ide-monaco": "workspace:*",
4239
"@opensumi/ide-overlay": "workspace:*",
4340
"@opensumi/ide-preferences": "workspace:*",
@@ -48,6 +45,7 @@
4845
"@xterm/xterm": "5.5.0",
4946
"ai": "^4.1.21",
5047
"ansi-regex": "^2.0.0",
48+
"diff": "^7.0.0",
5149
"dom-align": "^1.7.0",
5250
"rc-collapse": "^4.0.0",
5351
"react-chat-elements": "^12.0.10",

packages/ai-native/src/browser/ai-core.contribution.ts

+4
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,10 @@ export class AINativeBrowserContribution
412412
id: AINativeSettingSectionsId.CodeEditsTyping,
413413
localized: 'preference.ai.native.codeEdits.typing',
414414
},
415+
{
416+
id: AINativeSettingSectionsId.SystemPrompt,
417+
localized: 'preference.ai.native.chat.system.prompt',
418+
},
415419
],
416420
});
417421
}

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

+7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
ILogger,
1414
toDisposable,
1515
} from '@opensumi/ide-core-common';
16+
import { ChatMessageRole } from '@opensumi/ide-core-common';
1617
import { IChatMessage } from '@opensumi/ide-core-common/lib/types/ai-native';
1718

1819
import {
@@ -119,6 +120,12 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
119120
if (!data) {
120121
throw new Error(`No agent with id ${id}`);
121122
}
123+
if (data.agent.metadata.systemPrompt) {
124+
history.unshift({
125+
role: ChatMessageRole.System,
126+
content: data.agent.metadata.systemPrompt,
127+
});
128+
}
122129

123130
const result = await data.agent.invoke(request, progress, history, token);
124131
return result;

packages/ai-native/src/browser/chat/chat-model.ts

-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ export type IChatProgressResponseContent =
3434
| IChatComponent
3535
| IChatToolContent;
3636

37-
@Injectable({ multiple: true })
3837
export class ChatResponseModel extends Disposable {
3938
#responseParts: IChatProgressResponseContent[] = [];
4039
get responseParts() {
@@ -218,7 +217,6 @@ export class ChatResponseModel extends Disposable {
218217
}
219218
}
220219

221-
@Injectable({ multiple: true })
222220
export class ChatRequestModel implements IChatRequestModel {
223221
#requestId: string;
224222
public get requestId(): string {

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

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Autowired, Injectable } from '@opensumi/di';
2-
import { PreferenceService } from '@opensumi/ide-core-browser';
2+
import { AppConfig, PreferenceService } from '@opensumi/ide-core-browser';
33
import {
44
AIBackSerivcePath,
55
CancellationToken,
@@ -71,6 +71,9 @@ export class ChatProxyService extends Disposable {
7171
@Autowired(IMessageService)
7272
private readonly messageService: IMessageService;
7373

74+
@Autowired(AppConfig)
75+
private readonly appConfig: AppConfig;
76+
7477
private chatDeferred: Deferred<void> = new Deferred<void>();
7578

7679
public registerDefaultAgent() {
@@ -83,7 +86,14 @@ export class ChatProxyService extends Disposable {
8386
this.addDispose(
8487
this.chatAgentService.registerAgent({
8588
id: ChatProxyService.AGENT_ID,
86-
metadata: {},
89+
metadata: {
90+
systemPrompt:
91+
this.preferenceService.get<string>(
92+
AINativeSettingSectionsId.SystemPrompt,
93+
'You are a powerful AI coding assistant working in OpenSumi, a top IDE framework. You collaborate with a USER to solve coding tasks, which may involve creating, modifying, or debugging code, or answering questions. When the USER sends a message, relevant context (e.g., open files, cursor position, edit history, linter errors) may be attached. Use this information as needed.\n\n<tool_calling>\nYou have access to tools to assist with tasks. Follow these rules:\n1. Always adhere to the tool call schema and provide all required parameters.\n2. Only use tools explicitly provided; ignore unavailable ones.\n3. Avoid mentioning tool names to the USER (e.g., say "I will edit your file" instead of "I need to use the edit_file tool").\n4. Only call tools when necessary; respond directly if the task is general or you already know the answer.\n5. Explain to the USER why you’re using a tool before calling it.\n</tool_calling>\n\n<making_code_changes>\nWhen modifying code:\n1. Use code edit tools instead of outputting code unless explicitly requested.\n2. Limit tool calls to one per turn.\n3. Ensure generated code is immediately executable by including necessary imports, dependencies, and endpoints.\n4. For new projects, create a dependency management file (e.g., requirements.txt) and a README.\n5. For web apps, design a modern, user-friendly UI.\n6. Avoid generating non-textual or excessively long code.\n7. Read file contents before editing, unless appending a small change or creating a new file.\n8. Fix introduced linter errors if possible, but stop after 3 attempts and ask the USER for guidance.\n9. Reapply reasonable code edits if they weren’t followed initially.\n</making_code_changes>\n\nUse the appropriate tools to fulfill the USER’s request, ensuring all required parameters are provided or inferred from context.',
94+
) +
95+
`\n\n<user_info>\nThe user's OS version is ${this.applicationService.frontendOS}. The absolute path of the user's workspace is ${this.appConfig.workspaceDir}.\n</user_info>`,
96+
},
8797
invoke: async (
8898
request: IChatAgentRequest,
8999
progress: (part: IChatProgress) => void,

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

+12-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ export class ChatInternalService extends Disposable {
2727
private readonly _onChangeSession = new Emitter<string>();
2828
public readonly onChangeSession: Event<string> = this._onChangeSession.event;
2929

30+
private readonly _onCancelRequest = new Emitter<void>();
31+
public readonly onCancelRequest: Event<void> = this._onCancelRequest.event;
32+
33+
private readonly _onRegenerateRequest = new Emitter<void>();
34+
public readonly onRegenerateRequest: Event<void> = this._onRegenerateRequest.event;
35+
3036
private _latestRequestId: string;
3137
public get latestRequestId(): string {
3238
return this._latestRequestId;
@@ -52,11 +58,16 @@ export class ChatInternalService extends Disposable {
5258
}
5359

5460
sendRequest(request: ChatRequestModel, regenerate = false) {
55-
return this.chatManagerService.sendRequest(this.#sessionModel.sessionId, request, regenerate);
61+
const result = this.chatManagerService.sendRequest(this.#sessionModel.sessionId, request, regenerate);
62+
if (regenerate) {
63+
this._onRegenerateRequest.fire();
64+
}
65+
return result;
5666
}
5767

5868
cancelRequest() {
5969
this.chatManagerService.cancelRequest(this.#sessionModel.sessionId);
70+
this._onCancelRequest.fire();
6071
}
6172

6273
clearSessionModel() {

packages/ai-native/src/browser/components/ChatEditor.tsx

+13-10
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ interface Props {
3131
language?: string;
3232
agentId?: string;
3333
command?: string;
34+
hideInsert?: boolean;
3435
}
3536
export const CodeEditorWithHighlight = (props: Props) => {
36-
const { input, language, relationId, agentId, command } = props;
37+
const { input, language, relationId, agentId, command, hideInsert } = props;
3738
const ref = React.useRef<HTMLDivElement | null>(null);
3839
const monacoCommandRegistry = useInjectable<MonacoCommandRegistry>(MonacoCommandRegistry);
3940
const clipboardService = useInjectable<IClipboardService>(IClipboardService);
@@ -101,15 +102,17 @@ export const CodeEditorWithHighlight = (props: Props) => {
101102
return (
102103
<div className={styles.monaco_wrapper}>
103104
<div className={styles.action_toolbar}>
104-
<Popover id={`ai-chat-inser-${useUUID}`} title={localize('aiNative.chat.code.insert')}>
105-
<EnhanceIcon
106-
className={getIcon('insert')}
107-
onClick={() => handleInsert()}
108-
tabIndex={0}
109-
role='button'
110-
ariaLabel={localize('aiNative.chat.code.insert')}
111-
/>
112-
</Popover>
105+
{!hideInsert && (
106+
<Popover id={`ai-chat-inser-${useUUID}`} title={localize('aiNative.chat.code.insert')}>
107+
<EnhanceIcon
108+
className={getIcon('insert')}
109+
onClick={() => handleInsert()}
110+
tabIndex={0}
111+
role='button'
112+
ariaLabel={localize('aiNative.chat.code.insert')}
113+
/>
114+
</Popover>
115+
)}
113116
<Popover
114117
id={`ai-chat-copy-${useUUID}`}
115118
title={localize(isCoping ? 'aiNative.chat.code.copy.success' : 'aiNative.chat.code.copy')}

packages/ai-native/src/browser/components/ChatMarkdown.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface MarkdownProps {
1717
className?: string;
1818
fillInIncompleteTokens?: boolean; // 补齐不完整的 token,如代码块或表格
1919
markedOptions?: IMarkedOptions;
20+
hideInsert?: boolean;
2021
}
2122

2223
export const ChatMarkdown = (props: MarkdownProps) => {
@@ -42,13 +43,14 @@ export const ChatMarkdown = (props: MarkdownProps) => {
4243
<div className={styles.code}>
4344
<ConfigProvider value={appConfig}>
4445
<div className={styles.code_block}>
45-
<div className={styles.code_language}>{language}</div>
46+
<div className={cls(styles.code_language, 'language-badge')}>{language}</div>
4647
<CodeEditorWithHighlight
4748
input={code as string}
4849
language={language}
4950
relationId={props.relationId || ''}
5051
agentId={props.agentId}
5152
command={props.command}
53+
hideInsert={props.hideInsert}
5254
/>
5355
</div>
5456
</ConfigProvider>

packages/ai-native/src/browser/components/ChatReply.tsx

+8-15
Original file line numberDiff line numberDiff line change
@@ -149,16 +149,16 @@ const TreeRenderer = (props: { treeData: IChatResponseProgressFileTreeData }) =>
149149
);
150150
};
151151

152-
const ToolCallRender = (props: { toolCall: IChatToolContent['content'] }) => {
153-
const { toolCall } = props;
152+
const ToolCallRender = (props: { toolCall: IChatToolContent['content']; messageId?: string }) => {
153+
const { toolCall, messageId } = props;
154154
const chatAgentViewService = useInjectable<IChatAgentViewService>(ChatAgentViewServiceToken);
155155
const [node, setNode] = useState<React.JSX.Element | null>(null);
156156

157157
useEffect(() => {
158158
const config = chatAgentViewService.getChatComponent('toolCall');
159159
if (config) {
160160
const { component: Component, initialProps } = config;
161-
setNode(<Component {...initialProps} value={toolCall} />);
161+
setNode(<Component {...initialProps} value={toolCall} messageId={messageId} />);
162162
return;
163163
}
164164
setNode(
@@ -169,22 +169,22 @@ const ToolCallRender = (props: { toolCall: IChatToolContent['content'] }) => {
169169
);
170170
const deferred = chatAgentViewService.getChatComponentDeferred('toolCall')!;
171171
deferred.promise.then(({ component: Component, initialProps }) => {
172-
setNode(<Component {...initialProps} value={toolCall} />);
172+
setNode(<Component {...initialProps} value={toolCall} messageId={messageId} />);
173173
});
174174
}, [toolCall.state]);
175175

176176
return node;
177177
};
178178

179-
const ComponentRender = (props: { component: string; value?: unknown }) => {
179+
const ComponentRender = (props: { component: string; value?: unknown; messageId?: string }) => {
180180
const chatAgentViewService = useInjectable<IChatAgentViewService>(ChatAgentViewServiceToken);
181181
const [node, setNode] = useState<React.JSX.Element | null>(null);
182182

183183
useEffect(() => {
184184
const config = chatAgentViewService.getChatComponent(props.component);
185185
if (config) {
186186
const { component: Component, initialProps } = config;
187-
setNode(<Component {...initialProps} value={props.value} />);
187+
setNode(<Component {...initialProps} value={props.value} messageId={props.messageId} />);
188188
return;
189189
}
190190
setNode(
@@ -224,7 +224,6 @@ export const ChatReply = (props: IChatReplyProps) => {
224224
const chatApiService = useInjectable<ChatService>(ChatServiceToken);
225225
const chatAgentService = useInjectable<IChatAgentService>(IChatAgentService);
226226
const chatRenderRegistry = useInjectable<ChatRenderRegistry>(ChatRenderRegistryToken);
227-
228227
useEffect(() => {
229228
const disposableCollection = new DisposableCollection();
230229

@@ -298,12 +297,6 @@ export const ChatReply = (props: IChatReplyProps) => {
298297
</div>
299298
);
300299

301-
const renderComponent = (componentId: string, value: unknown) => (
302-
<ComponentRender component={componentId} value={value} />
303-
);
304-
305-
const renderToolCall = (toolCall: IChatToolContent['content']) => <ToolCallRender toolCall={toolCall} />;
306-
307300
const contentNode = React.useMemo(
308301
() =>
309302
request.response.responseContents.map((item, index) => {
@@ -313,9 +306,9 @@ export const ChatReply = (props: IChatReplyProps) => {
313306
} else if (item.kind === 'treeData') {
314307
node = renderTreeData(item.treeData);
315308
} else if (item.kind === 'component') {
316-
node = renderComponent(item.component, item.value);
309+
node = <ComponentRender component={item.component} value={item.value} messageId={msgId} />;
317310
} else if (item.kind === 'toolCall') {
318-
node = renderToolCall(item.content);
311+
node = <ToolCallRender toolCall={item.content} messageId={msgId} />;
319312
} else {
320313
node = renderMarkdown(item.content);
321314
}

0 commit comments

Comments
 (0)