mirror of https://github.com/opensumi/core
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 <retrox.jcy@alipay.com> Co-authored-by: John <qingyi.xjh@antgroup.com>
This commit is contained in:
parent
29f48cdeab
commit
05a43a6fd0
|
@ -94,16 +94,24 @@ Object.defineProperty(window, 'matchMedia', {
|
|||
Object.defineProperty(window, 'crypto', {
|
||||
writable: true,
|
||||
value: {
|
||||
randomUUID: () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
}),
|
||||
randomUUID: () =>
|
||||
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
}),
|
||||
getRandomValues: (array) => {
|
||||
if (!(array instanceof Int8Array || array instanceof Uint8Array ||
|
||||
array instanceof Int16Array || array instanceof Uint16Array ||
|
||||
array instanceof Int32Array || array instanceof Uint32Array ||
|
||||
array instanceof Uint8ClampedArray)) {
|
||||
if (
|
||||
!(
|
||||
array instanceof Int8Array ||
|
||||
array instanceof Uint8Array ||
|
||||
array instanceof Int16Array ||
|
||||
array instanceof Uint16Array ||
|
||||
array instanceof Int32Array ||
|
||||
array instanceof Uint32Array ||
|
||||
array instanceof Uint8ClampedArray
|
||||
)
|
||||
) {
|
||||
throw new TypeError('Expected a TypedArray');
|
||||
}
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
|
|
|
@ -56,6 +56,9 @@ describe('ChatAgentService', () => {
|
|||
const agent = {
|
||||
id: 'agent1',
|
||||
invoke: jest.fn().mockResolvedValue({}),
|
||||
metadata: {
|
||||
systemPrompt: 'You are a helpful assistant.',
|
||||
},
|
||||
} as unknown as IChatAgent;
|
||||
chatAgentService.registerAgent(agent);
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
"@ai-sdk/anthropic": "^1.1.6",
|
||||
"@ai-sdk/deepseek": "^0.1.8",
|
||||
"@ai-sdk/openai": "^1.1.9",
|
||||
"@anthropic-ai/sdk": "^0.36.3",
|
||||
"@modelcontextprotocol/sdk": "^1.3.1",
|
||||
"@opensumi/ide-addons": "workspace:*",
|
||||
"@opensumi/ide-components": "workspace:*",
|
||||
|
@ -34,10 +33,8 @@
|
|||
"@opensumi/ide-editor": "workspace:*",
|
||||
"@opensumi/ide-file-search": "workspace:*",
|
||||
"@opensumi/ide-file-service": "workspace:*",
|
||||
"@opensumi/ide-file-tree-next": "workspace:*",
|
||||
"@opensumi/ide-main-layout": "workspace:*",
|
||||
"@opensumi/ide-markers": "workspace:*",
|
||||
"@opensumi/ide-menu-bar": "workspace:*",
|
||||
"@opensumi/ide-monaco": "workspace:*",
|
||||
"@opensumi/ide-overlay": "workspace:*",
|
||||
"@opensumi/ide-preferences": "workspace:*",
|
||||
|
@ -48,6 +45,7 @@
|
|||
"@xterm/xterm": "5.5.0",
|
||||
"ai": "^4.1.21",
|
||||
"ansi-regex": "^2.0.0",
|
||||
"diff": "^7.0.0",
|
||||
"dom-align": "^1.7.0",
|
||||
"rc-collapse": "^4.0.0",
|
||||
"react-chat-elements": "^12.0.10",
|
||||
|
|
|
@ -412,6 +412,10 @@ export class AINativeBrowserContribution
|
|||
id: AINativeSettingSectionsId.CodeEditsTyping,
|
||||
localized: 'preference.ai.native.codeEdits.typing',
|
||||
},
|
||||
{
|
||||
id: AINativeSettingSectionsId.SystemPrompt,
|
||||
localized: 'preference.ai.native.chat.system.prompt',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
ILogger,
|
||||
toDisposable,
|
||||
} from '@opensumi/ide-core-common';
|
||||
import { ChatMessageRole } from '@opensumi/ide-core-common';
|
||||
import { IChatMessage } from '@opensumi/ide-core-common/lib/types/ai-native';
|
||||
|
||||
import {
|
||||
|
@ -119,6 +120,12 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
|
|||
if (!data) {
|
||||
throw new Error(`No agent with id ${id}`);
|
||||
}
|
||||
if (data.agent.metadata.systemPrompt) {
|
||||
history.unshift({
|
||||
role: ChatMessageRole.System,
|
||||
content: data.agent.metadata.systemPrompt,
|
||||
});
|
||||
}
|
||||
|
||||
const result = await data.agent.invoke(request, progress, history, token);
|
||||
return result;
|
||||
|
|
|
@ -34,7 +34,6 @@ export type IChatProgressResponseContent =
|
|||
| IChatComponent
|
||||
| IChatToolContent;
|
||||
|
||||
@Injectable({ multiple: true })
|
||||
export class ChatResponseModel extends Disposable {
|
||||
#responseParts: IChatProgressResponseContent[] = [];
|
||||
get responseParts() {
|
||||
|
@ -218,7 +217,6 @@ export class ChatResponseModel extends Disposable {
|
|||
}
|
||||
}
|
||||
|
||||
@Injectable({ multiple: true })
|
||||
export class ChatRequestModel implements IChatRequestModel {
|
||||
#requestId: string;
|
||||
public get requestId(): string {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Autowired, Injectable } from '@opensumi/di';
|
||||
import { PreferenceService } from '@opensumi/ide-core-browser';
|
||||
import { AppConfig, PreferenceService } from '@opensumi/ide-core-browser';
|
||||
import {
|
||||
AIBackSerivcePath,
|
||||
CancellationToken,
|
||||
|
@ -71,6 +71,9 @@ export class ChatProxyService extends Disposable {
|
|||
@Autowired(IMessageService)
|
||||
private readonly messageService: IMessageService;
|
||||
|
||||
@Autowired(AppConfig)
|
||||
private readonly appConfig: AppConfig;
|
||||
|
||||
private chatDeferred: Deferred<void> = new Deferred<void>();
|
||||
|
||||
public registerDefaultAgent() {
|
||||
|
@ -83,7 +86,14 @@ export class ChatProxyService extends Disposable {
|
|||
this.addDispose(
|
||||
this.chatAgentService.registerAgent({
|
||||
id: ChatProxyService.AGENT_ID,
|
||||
metadata: {},
|
||||
metadata: {
|
||||
systemPrompt:
|
||||
this.preferenceService.get<string>(
|
||||
AINativeSettingSectionsId.SystemPrompt,
|
||||
'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.',
|
||||
) +
|
||||
`\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>`,
|
||||
},
|
||||
invoke: async (
|
||||
request: IChatAgentRequest,
|
||||
progress: (part: IChatProgress) => void,
|
||||
|
|
|
@ -27,6 +27,12 @@ export class ChatInternalService extends Disposable {
|
|||
private readonly _onChangeSession = new Emitter<string>();
|
||||
public readonly onChangeSession: Event<string> = this._onChangeSession.event;
|
||||
|
||||
private readonly _onCancelRequest = new Emitter<void>();
|
||||
public readonly onCancelRequest: Event<void> = this._onCancelRequest.event;
|
||||
|
||||
private readonly _onRegenerateRequest = new Emitter<void>();
|
||||
public readonly onRegenerateRequest: Event<void> = this._onRegenerateRequest.event;
|
||||
|
||||
private _latestRequestId: string;
|
||||
public get latestRequestId(): string {
|
||||
return this._latestRequestId;
|
||||
|
@ -52,11 +58,16 @@ export class ChatInternalService extends Disposable {
|
|||
}
|
||||
|
||||
sendRequest(request: ChatRequestModel, regenerate = false) {
|
||||
return this.chatManagerService.sendRequest(this.#sessionModel.sessionId, request, regenerate);
|
||||
const result = this.chatManagerService.sendRequest(this.#sessionModel.sessionId, request, regenerate);
|
||||
if (regenerate) {
|
||||
this._onRegenerateRequest.fire();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
cancelRequest() {
|
||||
this.chatManagerService.cancelRequest(this.#sessionModel.sessionId);
|
||||
this._onCancelRequest.fire();
|
||||
}
|
||||
|
||||
clearSessionModel() {
|
||||
|
|
|
@ -31,9 +31,10 @@ interface Props {
|
|||
language?: string;
|
||||
agentId?: string;
|
||||
command?: string;
|
||||
hideInsert?: boolean;
|
||||
}
|
||||
export const CodeEditorWithHighlight = (props: Props) => {
|
||||
const { input, language, relationId, agentId, command } = props;
|
||||
const { input, language, relationId, agentId, command, hideInsert } = props;
|
||||
const ref = React.useRef<HTMLDivElement | null>(null);
|
||||
const monacoCommandRegistry = useInjectable<MonacoCommandRegistry>(MonacoCommandRegistry);
|
||||
const clipboardService = useInjectable<IClipboardService>(IClipboardService);
|
||||
|
@ -101,15 +102,17 @@ export const CodeEditorWithHighlight = (props: Props) => {
|
|||
return (
|
||||
<div className={styles.monaco_wrapper}>
|
||||
<div className={styles.action_toolbar}>
|
||||
<Popover id={`ai-chat-inser-${useUUID}`} title={localize('aiNative.chat.code.insert')}>
|
||||
<EnhanceIcon
|
||||
className={getIcon('insert')}
|
||||
onClick={() => handleInsert()}
|
||||
tabIndex={0}
|
||||
role='button'
|
||||
ariaLabel={localize('aiNative.chat.code.insert')}
|
||||
/>
|
||||
</Popover>
|
||||
{!hideInsert && (
|
||||
<Popover id={`ai-chat-inser-${useUUID}`} title={localize('aiNative.chat.code.insert')}>
|
||||
<EnhanceIcon
|
||||
className={getIcon('insert')}
|
||||
onClick={() => handleInsert()}
|
||||
tabIndex={0}
|
||||
role='button'
|
||||
ariaLabel={localize('aiNative.chat.code.insert')}
|
||||
/>
|
||||
</Popover>
|
||||
)}
|
||||
<Popover
|
||||
id={`ai-chat-copy-${useUUID}`}
|
||||
title={localize(isCoping ? 'aiNative.chat.code.copy.success' : 'aiNative.chat.code.copy')}
|
||||
|
|
|
@ -17,6 +17,7 @@ interface MarkdownProps {
|
|||
className?: string;
|
||||
fillInIncompleteTokens?: boolean; // 补齐不完整的 token,如代码块或表格
|
||||
markedOptions?: IMarkedOptions;
|
||||
hideInsert?: boolean;
|
||||
}
|
||||
|
||||
export const ChatMarkdown = (props: MarkdownProps) => {
|
||||
|
@ -42,13 +43,14 @@ export const ChatMarkdown = (props: MarkdownProps) => {
|
|||
<div className={styles.code}>
|
||||
<ConfigProvider value={appConfig}>
|
||||
<div className={styles.code_block}>
|
||||
<div className={styles.code_language}>{language}</div>
|
||||
<div className={cls(styles.code_language, 'language-badge')}>{language}</div>
|
||||
<CodeEditorWithHighlight
|
||||
input={code as string}
|
||||
language={language}
|
||||
relationId={props.relationId || ''}
|
||||
agentId={props.agentId}
|
||||
command={props.command}
|
||||
hideInsert={props.hideInsert}
|
||||
/>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
|
|
|
@ -149,8 +149,8 @@ const TreeRenderer = (props: { treeData: IChatResponseProgressFileTreeData }) =>
|
|||
);
|
||||
};
|
||||
|
||||
const ToolCallRender = (props: { toolCall: IChatToolContent['content'] }) => {
|
||||
const { toolCall } = props;
|
||||
const ToolCallRender = (props: { toolCall: IChatToolContent['content']; messageId?: string }) => {
|
||||
const { toolCall, messageId } = props;
|
||||
const chatAgentViewService = useInjectable<IChatAgentViewService>(ChatAgentViewServiceToken);
|
||||
const [node, setNode] = useState<React.JSX.Element | null>(null);
|
||||
|
||||
|
@ -158,7 +158,7 @@ const ToolCallRender = (props: { toolCall: IChatToolContent['content'] }) => {
|
|||
const config = chatAgentViewService.getChatComponent('toolCall');
|
||||
if (config) {
|
||||
const { component: Component, initialProps } = config;
|
||||
setNode(<Component {...initialProps} value={toolCall} />);
|
||||
setNode(<Component {...initialProps} value={toolCall} messageId={messageId} />);
|
||||
return;
|
||||
}
|
||||
setNode(
|
||||
|
@ -169,14 +169,14 @@ const ToolCallRender = (props: { toolCall: IChatToolContent['content'] }) => {
|
|||
);
|
||||
const deferred = chatAgentViewService.getChatComponentDeferred('toolCall')!;
|
||||
deferred.promise.then(({ component: Component, initialProps }) => {
|
||||
setNode(<Component {...initialProps} value={toolCall} />);
|
||||
setNode(<Component {...initialProps} value={toolCall} messageId={messageId} />);
|
||||
});
|
||||
}, [toolCall.state]);
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
const ComponentRender = (props: { component: string; value?: unknown }) => {
|
||||
const ComponentRender = (props: { component: string; value?: unknown; messageId?: string }) => {
|
||||
const chatAgentViewService = useInjectable<IChatAgentViewService>(ChatAgentViewServiceToken);
|
||||
const [node, setNode] = useState<React.JSX.Element | null>(null);
|
||||
|
||||
|
@ -184,7 +184,7 @@ const ComponentRender = (props: { component: string; value?: unknown }) => {
|
|||
const config = chatAgentViewService.getChatComponent(props.component);
|
||||
if (config) {
|
||||
const { component: Component, initialProps } = config;
|
||||
setNode(<Component {...initialProps} value={props.value} />);
|
||||
setNode(<Component {...initialProps} value={props.value} messageId={props.messageId} />);
|
||||
return;
|
||||
}
|
||||
setNode(
|
||||
|
@ -224,7 +224,6 @@ export const ChatReply = (props: IChatReplyProps) => {
|
|||
const chatApiService = useInjectable<ChatService>(ChatServiceToken);
|
||||
const chatAgentService = useInjectable<IChatAgentService>(IChatAgentService);
|
||||
const chatRenderRegistry = useInjectable<ChatRenderRegistry>(ChatRenderRegistryToken);
|
||||
|
||||
useEffect(() => {
|
||||
const disposableCollection = new DisposableCollection();
|
||||
|
||||
|
@ -298,12 +297,6 @@ export const ChatReply = (props: IChatReplyProps) => {
|
|||
</div>
|
||||
);
|
||||
|
||||
const renderComponent = (componentId: string, value: unknown) => (
|
||||
<ComponentRender component={componentId} value={value} />
|
||||
);
|
||||
|
||||
const renderToolCall = (toolCall: IChatToolContent['content']) => <ToolCallRender toolCall={toolCall} />;
|
||||
|
||||
const contentNode = React.useMemo(
|
||||
() =>
|
||||
request.response.responseContents.map((item, index) => {
|
||||
|
@ -313,9 +306,9 @@ export const ChatReply = (props: IChatReplyProps) => {
|
|||
} else if (item.kind === 'treeData') {
|
||||
node = renderTreeData(item.treeData);
|
||||
} else if (item.kind === 'component') {
|
||||
node = renderComponent(item.component, item.value);
|
||||
node = <ComponentRender component={item.component} value={item.value} messageId={msgId} />;
|
||||
} else if (item.kind === 'toolCall') {
|
||||
node = renderToolCall(item.content);
|
||||
node = <ToolCallRender toolCall={item.content} messageId={msgId} />;
|
||||
} else {
|
||||
node = renderMarkdown(item.content);
|
||||
}
|
||||
|
|
|
@ -1,20 +1,27 @@
|
|||
import cls from 'classnames';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { useInjectable } from '@opensumi/ide-core-browser';
|
||||
import { Icon } from '@opensumi/ide-core-browser/lib/components';
|
||||
import { Loading } from '@opensumi/ide-core-browser/lib/components/ai-native';
|
||||
import { IChatToolContent, uuid } from '@opensumi/ide-core-common';
|
||||
|
||||
import { IMCPServerRegistry, TokenMCPServerRegistry } from '../types';
|
||||
|
||||
import { CodeEditorWithHighlight } from './ChatEditor';
|
||||
import styles from './ChatToolRender.module.less';
|
||||
|
||||
export const ChatToolRender = (props: { value: IChatToolContent['content'] }) => {
|
||||
const { value } = props;
|
||||
export const ChatToolRender = (props: { value: IChatToolContent['content']; messageId?: string }) => {
|
||||
const { value, messageId } = props;
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const mcpServerFeatureRegistry = useInjectable<IMCPServerRegistry>(TokenMCPServerRegistry);
|
||||
|
||||
if (!value || !value.function || !value.id) {
|
||||
return null;
|
||||
}
|
||||
const label = mcpServerFeatureRegistry.getMCPTool(value.function.name)?.label || value.function.name;
|
||||
|
||||
const ToolComponent = mcpServerFeatureRegistry.getToolComponent(value.function.name);
|
||||
|
||||
const getStateInfo = (state?: string): { label: string; icon: React.ReactNode } => {
|
||||
switch (state) {
|
||||
|
@ -22,11 +29,22 @@ export const ChatToolRender = (props: { value: IChatToolContent['content'] }) =>
|
|||
case 'streaming':
|
||||
return { label: 'Generating', icon: <Loading /> };
|
||||
case 'complete':
|
||||
return { label: 'Complete', icon: <Icon iconClass="codicon codicon-check" /> };
|
||||
return { label: 'Complete', icon: <Icon iconClass='codicon codicon-check' /> };
|
||||
case 'result':
|
||||
return { label: 'Result Ready', icon: <Icon iconClass="codicon codicon-check-all" /> };
|
||||
return { label: 'Result Ready', icon: <Icon iconClass='codicon codicon-check-all' /> };
|
||||
default:
|
||||
return { label: state || 'Unknown', icon: <Icon iconClass="codicon codicon-question" /> };
|
||||
return { label: state || 'Unknown', icon: <Icon iconClass='codicon codicon-question' /> };
|
||||
}
|
||||
};
|
||||
const getParsedArgs = () => {
|
||||
try {
|
||||
// TODO: 流式输出中function_call的参数还不完整,需要等待complete状态
|
||||
if (value.state !== 'complete' && value.state !== 'result') {
|
||||
return {};
|
||||
}
|
||||
return JSON.parse(value.function?.arguments || '{}');
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -36,12 +54,12 @@ export const ChatToolRender = (props: { value: IChatToolContent['content'] }) =>
|
|||
|
||||
const stateInfo = getStateInfo(value.state);
|
||||
|
||||
return (
|
||||
<div className={styles['chat-tool-render']}>
|
||||
return [
|
||||
<div className={styles['chat-tool-render']} key='chat-tool-render'>
|
||||
<div className={styles['tool-header']} onClick={toggleExpand}>
|
||||
<div className={styles['tool-name']}>
|
||||
<span className={cls(styles['expand-icon'], { [styles.expanded]: isExpanded })}>▶</span>
|
||||
{value?.function?.name}
|
||||
{label}
|
||||
</div>
|
||||
{value.state && (
|
||||
<div className={styles['tool-state']}>
|
||||
|
@ -54,24 +72,27 @@ export const ChatToolRender = (props: { value: IChatToolContent['content'] }) =>
|
|||
{value?.function?.arguments && (
|
||||
<div className={styles['tool-arguments']}>
|
||||
<div className={styles['section-label']}>Arguments</div>
|
||||
<CodeEditorWithHighlight
|
||||
input={value?.function?.arguments}
|
||||
language={'json'}
|
||||
relationId={uuid(4)}
|
||||
/>
|
||||
<CodeEditorWithHighlight input={value?.function?.arguments} language={'json'} relationId={uuid(4)} />
|
||||
</div>
|
||||
)}
|
||||
{value?.result && (
|
||||
<div className={styles['tool-result']}>
|
||||
<div className={styles['section-label']}>Result</div>
|
||||
<CodeEditorWithHighlight
|
||||
input={value.result}
|
||||
language={'json'}
|
||||
relationId={uuid(4)}
|
||||
/>
|
||||
<CodeEditorWithHighlight input={value.result} language={'json'} relationId={uuid(4)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</div>,
|
||||
ToolComponent && (
|
||||
<ToolComponent
|
||||
key='tool-component'
|
||||
state={value.state}
|
||||
args={getParsedArgs()}
|
||||
result={value.result}
|
||||
index={value.index}
|
||||
messageId={messageId}
|
||||
toolCallId={value.id}
|
||||
/>
|
||||
),
|
||||
];
|
||||
};
|
||||
|
|
|
@ -253,7 +253,7 @@
|
|||
.editor {
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
padding: 32px 8px 8px 8px;
|
||||
padding: 28px 8px 8px 8px;
|
||||
line-height: 18px;
|
||||
&::-webkit-scrollbar {
|
||||
width: auto;
|
||||
|
@ -265,7 +265,7 @@
|
|||
display: flex;
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 6px;
|
||||
top: 2px;
|
||||
z-index: 100;
|
||||
height: 20px;
|
||||
align-items: center;
|
||||
|
@ -300,6 +300,7 @@
|
|||
background-color: var(--design-language-background);
|
||||
border-radius: 8px 0px 8px 0;
|
||||
color: var(--design-text-foreground);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -57,6 +57,7 @@ import { LanguageParserService } from './languages/service';
|
|||
import { MCPServerProxyService } from './mcp/mcp-server-proxy.service';
|
||||
import { MCPServerRegistry } from './mcp/mcp-server.feature.registry';
|
||||
import { CreateNewFileWithTextTool } from './mcp/tools/createNewFileWithText';
|
||||
import { EditFileTool } from './mcp/tools/editFile';
|
||||
import { FindFilesByNameSubstringTool } from './mcp/tools/findFilesByNameSubstring';
|
||||
import { GetCurrentFilePathTool } from './mcp/tools/getCurrentFilePath';
|
||||
import { GetDiagnosticsByPathTool } from './mcp/tools/getDiagnosticsByPath';
|
||||
|
@ -97,6 +98,7 @@ export class AINativeModule extends BrowserModule {
|
|||
// MCP Server Contributions START
|
||||
ListDirTool,
|
||||
ReadFileTool,
|
||||
EditFileTool,
|
||||
CreateNewFileWithTextTool,
|
||||
GetSelectedTextTool,
|
||||
GetOpenEditorFileDiagnosticsTool,
|
||||
|
|
|
@ -0,0 +1,349 @@
|
|||
import { createPatch } from 'diff';
|
||||
|
||||
import { Autowired } from '@opensumi/di';
|
||||
import { AppConfig, ChatMessageRole, IMarker, MarkerSeverity, OnEvent, WithEventBus } from '@opensumi/ide-core-browser';
|
||||
import { WorkbenchEditorService } from '@opensumi/ide-editor';
|
||||
import { EditorGroupCloseEvent } from '@opensumi/ide-editor/lib/browser';
|
||||
import { IMarkerService } from '@opensumi/ide-markers';
|
||||
import { Position, Range, Selection, SelectionDirection } from '@opensumi/ide-monaco';
|
||||
import { observableValue, transaction } from '@opensumi/ide-monaco/lib/common/observable';
|
||||
import { Deferred, URI, path } from '@opensumi/ide-utils';
|
||||
|
||||
import { IChatInternalService } from '../../common';
|
||||
import { ChatInternalService } from '../chat/chat.internal.service';
|
||||
import {
|
||||
BaseInlineDiffPreviewer,
|
||||
InlineDiffController,
|
||||
InlineDiffService,
|
||||
LiveInlineDiffPreviewer,
|
||||
} from '../widget/inline-diff';
|
||||
import { InlineStreamDiffHandler } from '../widget/inline-stream-diff/inline-stream-diff.handler';
|
||||
|
||||
import { FileHandler } from './tools/handlers/ReadFile';
|
||||
|
||||
// 提供代码块的唯一索引,迭代轮次,生成状态管理(包括取消),关联文件位置这些信息的记录,后续并行 apply 的支持
|
||||
export abstract class BaseApplyService extends WithEventBus {
|
||||
@Autowired(FileHandler)
|
||||
protected fileHandler: FileHandler;
|
||||
|
||||
@Autowired(IChatInternalService)
|
||||
protected chatInternalService: ChatInternalService;
|
||||
|
||||
@Autowired(AppConfig)
|
||||
protected appConfig: AppConfig;
|
||||
|
||||
@Autowired(WorkbenchEditorService)
|
||||
protected readonly editorService: WorkbenchEditorService;
|
||||
|
||||
@Autowired(InlineDiffService)
|
||||
private readonly inlineDiffService: InlineDiffService;
|
||||
|
||||
@Autowired(IMarkerService)
|
||||
private readonly markerService: IMarkerService;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addDispose(
|
||||
this.chatInternalService.onCancelRequest(() => {
|
||||
this.cancelAllApply();
|
||||
}),
|
||||
);
|
||||
this.addDispose(
|
||||
this.chatInternalService.onRegenerateRequest(() => {
|
||||
const messages = this.chatInternalService.sessionModel.history.getMessages();
|
||||
const messageId = messages[messages.length - 1].id;
|
||||
messageId && this.disposeApplyForMessage(messageId);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public readonly codeBlockMapObservable = observableValue<Map<string, CodeBlockData>>(this, new Map());
|
||||
|
||||
private activePreviewer: BaseInlineDiffPreviewer<InlineStreamDiffHandler> | undefined;
|
||||
|
||||
private pendingApplyParams:
|
||||
| {
|
||||
relativePath: string;
|
||||
newContent: string;
|
||||
range?: Range;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
@OnEvent(EditorGroupCloseEvent)
|
||||
onEditorGroupClose(event: EditorGroupCloseEvent) {
|
||||
if (this.activePreviewer?.getNode()?.uri.path.toString() === event.payload.resource.uri.path.toString()) {
|
||||
this.activePreviewer.dispose();
|
||||
this.activePreviewer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the code block data by relative or absolute path of the last assistant message
|
||||
*/
|
||||
getCodeBlock(relativeOrAbsolutePath: string, messageId?: string): CodeBlockData | undefined {
|
||||
if (!relativeOrAbsolutePath) {
|
||||
return undefined;
|
||||
}
|
||||
const blockId = this.generateBlockId(relativeOrAbsolutePath, messageId);
|
||||
return this.codeBlockMapObservable.get().get(blockId);
|
||||
}
|
||||
|
||||
getCodeBlockById(id: string): CodeBlockData | undefined {
|
||||
return this.codeBlockMapObservable.get().get(id);
|
||||
}
|
||||
|
||||
protected updateCodeBlock(codeBlock: CodeBlockData) {
|
||||
const codeBlockMap = new Map(this.codeBlockMapObservable.get());
|
||||
codeBlockMap.set(codeBlock.id, codeBlock);
|
||||
transaction((tx) => {
|
||||
this.codeBlockMapObservable.set(codeBlockMap, tx);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new code block and return its unique ID
|
||||
*/
|
||||
registerCodeBlock(relativePath: string, content: string): string {
|
||||
const blockId = this.generateBlockId(relativePath);
|
||||
|
||||
if (!this.codeBlockMapObservable.get().has(blockId)) {
|
||||
this.codeBlockMapObservable.get().set(blockId, {
|
||||
id: blockId,
|
||||
content,
|
||||
relativePath,
|
||||
status: 'generating',
|
||||
iterationCount: 0,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
return blockId;
|
||||
}
|
||||
|
||||
initToolCallId(blockId: string, toolCallId: string): void {
|
||||
const blockData = this.getCodeBlockById(blockId);
|
||||
if (blockData && !blockData.initToolCallId) {
|
||||
blockData.initToolCallId = toolCallId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply changes of a code block
|
||||
*/
|
||||
async apply(relativePath: string, newContent: string, instructions?: string): Promise<CodeBlockData> {
|
||||
const blockData = this.getCodeBlock(relativePath);
|
||||
if (!blockData) {
|
||||
throw new Error('Code block not found');
|
||||
}
|
||||
try {
|
||||
if (++blockData.iterationCount > 3) {
|
||||
throw new Error('Max iteration count exceeded');
|
||||
}
|
||||
blockData.status = 'generating';
|
||||
blockData.content = newContent;
|
||||
this.updateCodeBlock(blockData);
|
||||
const applyDiffResult = await this.doApply(relativePath, newContent, instructions);
|
||||
blockData.applyResult = applyDiffResult;
|
||||
this.updateCodeBlock(blockData);
|
||||
return blockData;
|
||||
} catch (err) {
|
||||
blockData.status = 'failed';
|
||||
this.updateCodeBlock(blockData);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async reRenderPendingApply() {
|
||||
if (!this.pendingApplyParams) {
|
||||
throw new Error('No pending apply params');
|
||||
}
|
||||
const result = await this.renderApplyResult(
|
||||
this.pendingApplyParams.relativePath,
|
||||
this.pendingApplyParams.newContent,
|
||||
this.pendingApplyParams.range,
|
||||
);
|
||||
if (result) {
|
||||
const blockData = this.getCodeBlock(this.pendingApplyParams.relativePath)!;
|
||||
blockData.applyResult = result;
|
||||
this.updateCodeBlock(blockData);
|
||||
}
|
||||
}
|
||||
|
||||
async renderApplyResult(
|
||||
relativePath: string,
|
||||
newContent: string,
|
||||
range?: Range,
|
||||
): Promise<{ diff: string; diagnosticInfos: IMarker[] } | undefined> {
|
||||
// 用户可能会关闭编辑器,所以需要缓存参数
|
||||
this.pendingApplyParams = {
|
||||
relativePath,
|
||||
newContent,
|
||||
range,
|
||||
};
|
||||
const blockData = this.getCodeBlock(relativePath);
|
||||
if (!blockData) {
|
||||
throw new Error('Code block not found');
|
||||
}
|
||||
const openResult = await this.editorService.open(URI.file(path.join(this.appConfig.workspaceDir, relativePath)));
|
||||
if (!openResult) {
|
||||
throw new Error('Failed to open editor');
|
||||
}
|
||||
const editor = openResult.group.codeEditor.monacoEditor;
|
||||
const inlineDiffController = InlineDiffController.get(editor)!;
|
||||
blockData.status = 'pending';
|
||||
this.updateCodeBlock(blockData);
|
||||
|
||||
range = range || editor.getModel()?.getFullModelRange()!;
|
||||
// Create diff previewer
|
||||
const previewer = inlineDiffController.createDiffPreviewer(
|
||||
editor,
|
||||
Selection.fromRange(range, SelectionDirection.LTR),
|
||||
{
|
||||
disposeWhenEditorClosed: true,
|
||||
renderRemovedWidgetImmediately: true,
|
||||
},
|
||||
) as LiveInlineDiffPreviewer;
|
||||
this.activePreviewer = previewer;
|
||||
|
||||
const fullOriginalContent = editor.getModel()!.getValue();
|
||||
const savedContent = editor.getModel()!.getValueInRange(range);
|
||||
const deferred = new Deferred<{ diff: string; diagnosticInfos: IMarker[] }>();
|
||||
if (newContent === savedContent) {
|
||||
blockData.status = 'success';
|
||||
deferred.resolve();
|
||||
} else {
|
||||
previewer.setValue(newContent);
|
||||
this.addDispose(
|
||||
this.inlineDiffService.onPartialEdit((event) => {
|
||||
// TODO 支持自动保存
|
||||
if (event.totalPartialEditCount === event.resolvedPartialEditCount) {
|
||||
if (event.acceptPartialEditCount > 0) {
|
||||
blockData.status = 'success';
|
||||
const appliedResult = editor.getModel()!.getValue();
|
||||
const diffResult = createPatch(relativePath, fullOriginalContent, appliedResult)
|
||||
.split('\n')
|
||||
.slice(4)
|
||||
.join('\n');
|
||||
const rangesFromDiffHunk = diffResult
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
if (line.startsWith('@@')) {
|
||||
const [, , , start, end] = line.match(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@/)!;
|
||||
return new Range(parseInt(start, 10), 0, parseInt(end, 10), 0);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((range) => range !== null);
|
||||
const diagnosticInfos = this.getdiagnosticInfos(editor.getModel()!.uri.toString(), rangesFromDiffHunk);
|
||||
// 移除开头的几个固定信息,避免浪费 tokens
|
||||
deferred.resolve({
|
||||
diff: diffResult,
|
||||
diagnosticInfos,
|
||||
});
|
||||
} else {
|
||||
// 用户全部取消
|
||||
blockData.status = 'cancelled';
|
||||
deferred.resolve();
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel an ongoing apply operation
|
||||
*/
|
||||
cancelApply(relativePath: string): void {
|
||||
const blockData = this.getCodeBlock(relativePath);
|
||||
if (blockData && (blockData.status === 'generating' || blockData.status === 'pending')) {
|
||||
if (this.activePreviewer) {
|
||||
this.activePreviewer.getNode()?.livePreviewDiffDecorationModel.discardUnProcessed();
|
||||
this.activePreviewer.dispose();
|
||||
}
|
||||
blockData.status = 'cancelled';
|
||||
this.updateCodeBlock(blockData);
|
||||
}
|
||||
}
|
||||
|
||||
cancelAllApply(): void {
|
||||
this.codeBlockMapObservable.get().forEach((blockData) => {
|
||||
if (blockData.status === 'generating' || blockData.status === 'pending') {
|
||||
this.cancelApply(blockData.relativePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
disposeApplyForMessage(messageId: string): void {
|
||||
this.codeBlockMapObservable.get().forEach((blockData) => {
|
||||
if (blockData.id.endsWith(':' + messageId)) {
|
||||
if (blockData.status === 'generating') {
|
||||
this.cancelApply(blockData.relativePath);
|
||||
}
|
||||
this.codeBlockMapObservable.get().delete(blockData.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
revealApplyPosition(blockId: string): void {
|
||||
const blockData = this.codeBlockMapObservable.get().get(blockId);
|
||||
if (blockData) {
|
||||
const hunkInfo = blockData.applyResult?.diff.split('\n').find((line) => line.startsWith('@@'));
|
||||
let startLine = 0;
|
||||
let endLine = 0;
|
||||
if (hunkInfo) {
|
||||
// 取改动后的区间
|
||||
const [, , , start, end] = hunkInfo.match(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@/)!;
|
||||
startLine = parseInt(start, 10) - 1;
|
||||
endLine = parseInt(end, 10) - 1;
|
||||
}
|
||||
this.editorService.open(URI.file(path.join(this.appConfig.workspaceDir, blockData.relativePath)));
|
||||
const editor = this.editorService.currentEditor;
|
||||
if (editor) {
|
||||
editor.setSelection(new Selection(startLine, 0, endLine, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract doApply(
|
||||
relativePath: string,
|
||||
newContent: string,
|
||||
instructions?: string,
|
||||
): Promise<{ diff: string; diagnosticInfos: IMarker[] } | undefined>;
|
||||
|
||||
protected generateBlockId(absoluteOrRelativePath: string, messageId?: string): string {
|
||||
if (!absoluteOrRelativePath.startsWith('/')) {
|
||||
absoluteOrRelativePath = path.join(this.appConfig.workspaceDir, absoluteOrRelativePath);
|
||||
}
|
||||
const sessionId = this.chatInternalService.sessionModel.sessionId;
|
||||
const messages = this.chatInternalService.sessionModel.history.getMessages();
|
||||
messageId = messageId || messages[messages.length - 1].id;
|
||||
return `${sessionId}:${absoluteOrRelativePath}:${messageId || '-'}`;
|
||||
}
|
||||
|
||||
protected getdiagnosticInfos(uri: string, ranges: Range[]) {
|
||||
const markers = this.markerService.getManager().getMarkers({ resource: uri });
|
||||
return markers.filter(
|
||||
(marker) =>
|
||||
marker.severity >= MarkerSeverity.Warning &&
|
||||
ranges.some((range) => range.containsPosition(new Position(marker.startLineNumber, marker.startColumn))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface CodeBlockData {
|
||||
id: string;
|
||||
initToolCallId?: string;
|
||||
content: string;
|
||||
relativePath: string;
|
||||
status: CodeBlockStatus;
|
||||
iterationCount: number;
|
||||
createdAt: number;
|
||||
applyResult?: {
|
||||
diff: string;
|
||||
diagnosticInfos: IMarker[];
|
||||
};
|
||||
}
|
||||
|
||||
export type CodeBlockStatus = 'generating' | 'pending' | 'success' | 'rejected' | 'failed' | 'cancelled';
|
|
@ -1,3 +1,5 @@
|
|||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
import { Autowired, Injectable } from '@opensumi/di';
|
||||
import { ILogger } from '@opensumi/ide-core-browser';
|
||||
import { Emitter, Event } from '@opensumi/ide-core-common';
|
||||
|
@ -29,10 +31,10 @@ export class MCPServerProxyService implements IMCPServerProxyService {
|
|||
async $getMCPTools() {
|
||||
const tools = await this.mcpServerRegistry.getMCPTools().map((tool) =>
|
||||
// 不要传递 handler
|
||||
({
|
||||
({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
inputSchema: zodToJsonSchema(tool.inputSchema),
|
||||
providerName: 'sumi-builtin',
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
// OpenSumi as MCP Server 前端的代理服务
|
||||
import { Autowired, Injectable } from '@opensumi/di';
|
||||
import { IAIBackService, ILogger } from '@opensumi/ide-core-common';
|
||||
import { ILogger } from '@opensumi/ide-core-common';
|
||||
|
||||
import { IMCPServerRegistry, MCPLogger, MCPToolDefinition } from '../types';
|
||||
import { getToolName } from '../../common/utils';
|
||||
import { IMCPServerRegistry, IMCPServerToolComponentProps, MCPLogger, MCPToolDefinition } from '../types';
|
||||
|
||||
class LoggerAdapter implements MCPLogger {
|
||||
constructor(private readonly logger: ILogger) { }
|
||||
constructor(private readonly logger: ILogger) {}
|
||||
|
||||
appendLine(message: string): void {
|
||||
this.logger.log(message);
|
||||
|
@ -15,6 +16,7 @@ class LoggerAdapter implements MCPLogger {
|
|||
@Injectable()
|
||||
export class MCPServerRegistry implements IMCPServerRegistry {
|
||||
private tools: MCPToolDefinition[] = [];
|
||||
private toolComponents: Record<string, React.FC<IMCPServerToolComponentProps>> = {};
|
||||
|
||||
@Autowired(ILogger)
|
||||
private readonly baseLogger: ILogger;
|
||||
|
@ -23,10 +25,26 @@ export class MCPServerRegistry implements IMCPServerRegistry {
|
|||
return new LoggerAdapter(this.baseLogger);
|
||||
}
|
||||
|
||||
getMCPTool(name: string, serverName = 'sumi-builtin'): MCPToolDefinition | undefined {
|
||||
return this.tools.find((tool) => getToolName(tool.name, serverName) === name);
|
||||
}
|
||||
|
||||
registerMCPTool(tool: MCPToolDefinition): void {
|
||||
this.tools.push(tool);
|
||||
}
|
||||
|
||||
registerToolComponent(
|
||||
name: string,
|
||||
component: React.FC<IMCPServerToolComponentProps>,
|
||||
serverName = 'sumi-builtin',
|
||||
): void {
|
||||
this.toolComponents[getToolName(name, serverName)] = component;
|
||||
}
|
||||
|
||||
getToolComponent(name: string): React.FC<IMCPServerToolComponentProps> | undefined {
|
||||
return this.toolComponents[name];
|
||||
}
|
||||
|
||||
getMCPTools(): MCPToolDefinition[] {
|
||||
return this.tools;
|
||||
}
|
||||
|
@ -43,8 +61,12 @@ export class MCPServerRegistry implements IMCPServerRegistry {
|
|||
if (!tool) {
|
||||
throw new Error(`MCP tool ${name} not found`);
|
||||
}
|
||||
// 统一校验并转换
|
||||
args = tool.inputSchema.parse(args);
|
||||
return await tool.handler(args, this.logger);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('callMCPTool error:', error);
|
||||
return {
|
||||
content: [{ type: 'text', text: `The tool ${name} failed to execute. Error: ${error}` }],
|
||||
isError: true,
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
import cls from 'classnames';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
|
||||
import { Icon, Popover } from '@opensumi/ide-components';
|
||||
import {
|
||||
AppConfig,
|
||||
LabelService,
|
||||
MarkerSeverity,
|
||||
URI,
|
||||
Uri,
|
||||
detectModeId,
|
||||
path,
|
||||
useAutorun,
|
||||
useInjectable,
|
||||
} from '@opensumi/ide-core-browser';
|
||||
import { Loading } from '@opensumi/ide-core-browser/lib/components/ai-native';
|
||||
import { ILanguageService } from '@opensumi/monaco-editor-core/esm/vs/editor/common/languages/language';
|
||||
import { IModelService } from '@opensumi/monaco-editor-core/esm/vs/editor/common/services/model';
|
||||
import { StandaloneServices } from '@opensumi/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
|
||||
|
||||
import { ChatMarkdown } from '../../../components/ChatMarkdown';
|
||||
import { IMCPServerToolComponentProps } from '../../../types';
|
||||
import { BaseApplyService, CodeBlockData } from '../../base-apply.service';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
const renderStatus = (codeBlockData: CodeBlockData) => {
|
||||
const status = codeBlockData.status;
|
||||
switch (status) {
|
||||
case 'generating':
|
||||
return <Loading />;
|
||||
case 'pending':
|
||||
return (
|
||||
<Popover title={status} id={'edit-file-tool-status-pending'}>
|
||||
<Icon iconClass='codicon codicon-circle-large' />
|
||||
</Popover>
|
||||
);
|
||||
case 'success':
|
||||
return (
|
||||
<Popover title={status} id={'edit-file-tool-status-success'}>
|
||||
<Icon iconClass='codicon codicon-check-all' />
|
||||
</Popover>
|
||||
);
|
||||
case 'failed':
|
||||
return (
|
||||
<Popover title={status} id={'edit-file-tool-status-failed'}>
|
||||
<Icon iconClass='codicon codicon-error' color='var(--vscode-input-errorForeground)' />
|
||||
</Popover>
|
||||
);
|
||||
case 'cancelled':
|
||||
return (
|
||||
<Popover title={status} id={'edit-file-tool-status-cancelled'}>
|
||||
<Icon iconClass='codicon codicon-close' color='var(--vscode-input-placeholderForeground)' />
|
||||
</Popover>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const EditFileToolComponent = (props: IMCPServerToolComponentProps) => {
|
||||
const { args, messageId, toolCallId } = props;
|
||||
const labelService = useInjectable(LabelService);
|
||||
const appConfig = useInjectable<AppConfig>(AppConfig);
|
||||
const applyService = useInjectable<BaseApplyService>(BaseApplyService);
|
||||
const { target_file = '', code_edit, instructions } = args || {};
|
||||
const absolutePath = path.join(appConfig.workspaceDir, target_file);
|
||||
|
||||
const codeBlockData = applyService.getCodeBlock(absolutePath, messageId);
|
||||
|
||||
useAutorun(applyService.codeBlockMapObservable);
|
||||
|
||||
if (toolCallId && codeBlockData) {
|
||||
applyService.initToolCallId(codeBlockData.id, toolCallId);
|
||||
}
|
||||
|
||||
const icon = useMemo(() => {
|
||||
if (!target_file) {
|
||||
return;
|
||||
}
|
||||
const icon = `file-icon ${labelService.getIcon(URI.file(absolutePath))}`;
|
||||
return icon;
|
||||
}, [target_file, absolutePath]);
|
||||
const languageId = useMemo(() => {
|
||||
if (!target_file) {
|
||||
return;
|
||||
}
|
||||
const modelService = StandaloneServices.get(IModelService);
|
||||
const languageService = StandaloneServices.get(ILanguageService);
|
||||
const detectedModeId = detectModeId(modelService, languageService, Uri.file(absolutePath));
|
||||
return detectedModeId;
|
||||
}, [target_file, absolutePath]);
|
||||
|
||||
// 多次迭代时,仅在首处tool组件中展示
|
||||
if (!args || !codeBlockData || (toolCallId && toolCallId !== codeBlockData.initToolCallId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
instructions && <p>{instructions}</p>,
|
||||
<div className={styles['edit-file-tool']} key={`edit-file-tool-${codeBlockData.id}`}>
|
||||
<div
|
||||
className={cls(styles['edit-file-tool-header'], {
|
||||
clickable: codeBlockData.status === 'pending' || codeBlockData.status === 'success',
|
||||
})}
|
||||
onClick={() => {
|
||||
if (codeBlockData.status === 'pending') {
|
||||
applyService.reRenderPendingApply();
|
||||
} else if (codeBlockData.status === 'success') {
|
||||
applyService.revealApplyPosition(codeBlockData.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{icon && <span className={icon}></span>}
|
||||
<span className={styles['edit-file-tool-file-name']}>{target_file}</span>
|
||||
{codeBlockData.iterationCount > 1 && (
|
||||
<span className={styles['edit-file-tool-iteration-count']}>{codeBlockData.iterationCount}/3</span>
|
||||
)}
|
||||
{renderStatus(codeBlockData)}
|
||||
</div>
|
||||
<ChatMarkdown markdown={`\`\`\`${languageId || ''}\n${code_edit}\n\`\`\``} hideInsert={true} />
|
||||
</div>,
|
||||
codeBlockData.applyResult && codeBlockData.applyResult.diagnosticInfos.length > 0 && (
|
||||
<div
|
||||
className={styles['edit-file-tool-diagnostic-errors']}
|
||||
key={`edit-file-tool-diagnostic-errors-${codeBlockData.id}`}
|
||||
>
|
||||
<div className={styles['title']}>Found Lints:</div>
|
||||
{codeBlockData.applyResult?.diagnosticInfos.map((info) => (
|
||||
<div
|
||||
key={info.message}
|
||||
className={cls({
|
||||
[styles['error']]: info.severity === MarkerSeverity.Error,
|
||||
[styles['warning']]: info.severity === MarkerSeverity.Warning,
|
||||
})}
|
||||
>
|
||||
<Icon className={`codicon codicon-${info.severity === MarkerSeverity.Error ? 'error' : 'warning'}`} />
|
||||
{info.message.split('\n')[0]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
];
|
||||
};
|
|
@ -0,0 +1,67 @@
|
|||
.edit-file-tool-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-bottom: 1px solid var(--vscode-commandCenter-inactiveBorder);
|
||||
background-color: var(--design-block-background);
|
||||
font-size: 10px;
|
||||
margin-bottom: -4px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
> span {
|
||||
margin-right: 4px;
|
||||
}
|
||||
:global(span.codicon) {
|
||||
font-size: 12px;
|
||||
}
|
||||
:global(.kt-popover-trigger) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.edit-file-tool {
|
||||
border: 1px solid var(--vscode-commandCenter-inactiveBorder);
|
||||
border-radius: 4px;
|
||||
margin: 8px 0;
|
||||
:global(.language-badge) {
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
pre > code {
|
||||
border-radius: 0 0 8px 8px !important;
|
||||
}
|
||||
}
|
||||
.edit-file-tool-iteration-count {
|
||||
color: var(--vscode-input-placeholderForeground);
|
||||
margin-left: 4px;
|
||||
}
|
||||
:global(.clickable) {
|
||||
cursor: pointer;
|
||||
}
|
||||
.edit-file-tool-diagnostic-errors {
|
||||
padding: 8px;
|
||||
border: 1px solid var(--vscode-commandCenter-inactiveBorder);
|
||||
background-color: var(--design-block-background);
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
margin: 4px 0;
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 0;
|
||||
}
|
||||
:global(.codicon) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.title {
|
||||
margin-bottom: 3px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
.error,
|
||||
.error > span {
|
||||
color: var(--debugConsole-errorForeground);
|
||||
}
|
||||
.warning,
|
||||
.warning > span {
|
||||
color: var(--debugConsole-warningForeground);
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
import { Autowired } from '@opensumi/di';
|
||||
import { Domain, URI, path } from '@opensumi/ide-core-common';
|
||||
|
@ -38,7 +37,7 @@ export class CreateNewFileWithTextTool implements MCPServerContribution {
|
|||
'"ok" if the file was successfully created and populated, ' +
|
||||
'"can\'t find project dir" if the project directory cannot be determined. ' +
|
||||
'Note: Creates any necessary parent directories automatically.',
|
||||
inputSchema: zodToJsonSchema(inputSchema),
|
||||
inputSchema,
|
||||
handler: this.handler.bind(this),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { Autowired } from '@opensumi/di';
|
||||
import { Domain } from '@opensumi/ide-core-common';
|
||||
|
||||
import { IMCPServerRegistry, MCPLogger, MCPServerContribution, MCPToolDefinition } from '../../types';
|
||||
|
||||
import { EditFileToolComponent } from './components/EditFile';
|
||||
import { EditFileHandler } from './handlers/EditFile';
|
||||
const inputSchema = z
|
||||
.object({
|
||||
target_file: z
|
||||
.string()
|
||||
.describe(
|
||||
'The target file to modify. Always specify the target file as the first argument and use the relative path in the workspace of the file to edit',
|
||||
),
|
||||
instructions: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'A single sentence instruction describing what you are going to do for the sketched edit. This is used to assist the less intelligent model in applying the edit. Please use the first person to describe what you are going to do. Dont repeat what you have said previously in normal messages. And use it to disambiguate uncertainty in the edit.',
|
||||
),
|
||||
code_edit: z
|
||||
.string()
|
||||
.describe(
|
||||
"Specify ONLY the precise lines of code that you wish to edit. **NEVER specify or write out unchanged code**. Instead, represent all unchanged code using the comment of the language you're editing in - example: `// ... existing code ...`",
|
||||
),
|
||||
})
|
||||
.transform((data) => ({
|
||||
targetFile: data.target_file,
|
||||
instructions: data.instructions,
|
||||
codeEdit: data.code_edit,
|
||||
}));
|
||||
|
||||
@Domain(MCPServerContribution)
|
||||
export class EditFileTool implements MCPServerContribution {
|
||||
@Autowired(EditFileHandler)
|
||||
private readonly editFileHandler: EditFileHandler;
|
||||
|
||||
registerMCPServer(registry: IMCPServerRegistry): void {
|
||||
registry.registerMCPTool(this.getToolDefinition());
|
||||
registry.registerToolComponent('edit_file', EditFileToolComponent);
|
||||
}
|
||||
|
||||
getToolDefinition(): MCPToolDefinition {
|
||||
return {
|
||||
name: 'edit_file',
|
||||
label: 'Edit File',
|
||||
description: `Use this tool to propose an edit to an existing file.
|
||||
This will be read by a less intelligent model, which will quickly apply the edit. You should make it clear what the edit is, while also minimizing the unchanged code you write.
|
||||
When writing the edit, you should specify each edit in sequence, with the special comment \`// ... existing code ...\` to represent unchanged code in between edited lines.
|
||||
For example:
|
||||
\`\`\`
|
||||
// ... existing code ...
|
||||
FIRST_EDIT
|
||||
// ... existing code ...
|
||||
SECOND_EDIT
|
||||
// ... existing code ...
|
||||
THIRD_EDIT
|
||||
// ... existing code ...
|
||||
\`\`\`
|
||||
You should bias towards repeating as few lines of the original file as possible to convey the change.
|
||||
But, each edit should contain sufficient context of unchanged lines around the code you're editing to resolve ambiguity.
|
||||
DO NOT omit spans of pre-existing code without using the \`// ... existing code ...\` comment to indicate its absence.
|
||||
Make sure it is clear what the edit should be.
|
||||
You should specify the following arguments before the others: [target_file]`,
|
||||
inputSchema,
|
||||
handler: this.handler.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
private async handler(args: z.infer<typeof inputSchema>, logger: MCPLogger) {
|
||||
const result = await this.editFileHandler.handler(args.targetFile, args.codeEdit, args.instructions);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
// TODO: lint error
|
||||
text: result.applyResult
|
||||
? `The apply model made the following changes to the file:
|
||||
|
||||
\`\`\`
|
||||
${result.applyResult.diff}
|
||||
\`\`\`
|
||||
${
|
||||
result.applyResult.diagnosticInfos.length > 0
|
||||
? `The edit introduced the following new linter errors:
|
||||
${result.applyResult.diagnosticInfos
|
||||
.map((error) => `Line ${error.startLineNumber}: ${error.message.split('\n')[0]}`)
|
||||
.join('\n')}
|
||||
|
||||
Please fix the linter errors if it is clear how to (or you can easily figure out how to). Do not make uneducated guesses. And do not loop more than 3 times on fixing linter errors on the same file.`
|
||||
: ''
|
||||
}`
|
||||
: 'User cancelled the edit.',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import * as path from 'path';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
import { Autowired, Injectable } from '@opensumi/di';
|
||||
import { Domain, URI } from '@opensumi/ide-core-common';
|
||||
|
@ -38,7 +37,7 @@ export class FindFilesByNameSubstringTool implements MCPServerContribution {
|
|||
'- name: File name ' +
|
||||
'Returns an empty array ([]) if no matching files are found. ' +
|
||||
'Note: Only searches through files within the project directory, excluding libraries and external dependencies.',
|
||||
inputSchema: zodToJsonSchema(inputSchema),
|
||||
inputSchema,
|
||||
handler: this.handler.bind(this),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
import { Autowired, Injectable } from '@opensumi/di';
|
||||
import { Domain } from '@opensumi/ide-core-common';
|
||||
|
@ -25,7 +24,7 @@ export class GetCurrentFilePathTool implements MCPServerContribution {
|
|||
'Retrieves the absolute path of the currently active file in the VS Code editor. ' +
|
||||
'Use this tool to get the file location for tasks requiring file path information. ' +
|
||||
'Returns an empty string if no file is currently open.',
|
||||
inputSchema: zodToJsonSchema(inputSchema),
|
||||
inputSchema,
|
||||
handler: this.handler.bind(this),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import * as path from 'path';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
import { Autowired, Injectable } from '@opensumi/di';
|
||||
import { Domain, URI } from '@opensumi/ide-core-common';
|
||||
|
@ -47,7 +46,7 @@ export class GetDiagnosticsByPathTool implements MCPServerContribution {
|
|||
'- "error": Must be fixed immediately as they indicate critical issues that will prevent code from working correctly. ' +
|
||||
'- "warning": For user code, preserve unless the warning indicates a clear improvement opportunity. For generated code, optimize to remove warnings. ' +
|
||||
'- "information"/"hint": For user code, preserve as they might reflect intentional patterns. For generated code, optimize if it improves code quality without changing functionality.',
|
||||
inputSchema: zodToJsonSchema(inputSchema),
|
||||
inputSchema,
|
||||
handler: this.handler.bind(this),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import * as path from 'path';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
import { Autowired, Injectable } from '@opensumi/di';
|
||||
import { Domain, URI } from '@opensumi/ide-core-common';
|
||||
|
@ -38,7 +37,7 @@ export class GetFileTextByPathTool implements MCPServerContribution {
|
|||
'- error "project dir not found" if project directory cannot be determined ' +
|
||||
'- error "file not found" if the file doesn\'t exist or is outside project scope ' +
|
||||
'Note: Automatically refreshes the file system before reading',
|
||||
inputSchema: zodToJsonSchema(inputSchema),
|
||||
inputSchema,
|
||||
handler: this.handler.bind(this),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import * as path from 'path';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
import { Autowired, Injectable } from '@opensumi/di';
|
||||
import { Domain, URI } from '@opensumi/ide-core-common';
|
||||
|
@ -47,7 +46,7 @@ export class GetOpenEditorFileDiagnosticsTool implements MCPServerContribution {
|
|||
'- "error": Must be fixed immediately as they indicate critical issues that will prevent code from working correctly. ' +
|
||||
'- "warning": For user code, preserve unless the warning indicates a clear improvement opportunity. For generated code, optimize to remove warnings. ' +
|
||||
'- "information"/"hint": For user code, preserve as they might reflect intentional patterns. For generated code, optimize if it improves code quality without changing functionality.',
|
||||
inputSchema: zodToJsonSchema(inputSchema),
|
||||
inputSchema,
|
||||
handler: this.handler.bind(this),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
import { Autowired, Injectable } from '@opensumi/di';
|
||||
import { Domain } from '@opensumi/ide-core-common';
|
||||
|
@ -25,7 +24,7 @@ export class GetOpenEditorFileTextTool implements MCPServerContribution {
|
|||
'Retrieves the complete text content of the currently active file in the IDE editor. ' +
|
||||
"Use this tool to access and analyze the file's contents for tasks such as code review, content inspection, or text processing. " +
|
||||
'Returns empty string if no file is currently open.',
|
||||
inputSchema: zodToJsonSchema(inputSchema),
|
||||
inputSchema,
|
||||
handler: this.handler.bind(this),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
import { Autowired, Injectable } from '@opensumi/di';
|
||||
import { Domain } from '@opensumi/ide-core-common';
|
||||
|
@ -25,7 +24,7 @@ export class GetSelectedTextTool implements MCPServerContribution {
|
|||
'Retrieves the currently selected text from the active editor in VS Code. ' +
|
||||
'Use this tool when you need to access and analyze text that has been highlighted/selected by the user. ' +
|
||||
'Returns an empty string if no text is selected or no editor is open.',
|
||||
inputSchema: zodToJsonSchema(inputSchema),
|
||||
inputSchema,
|
||||
handler: this.handler.bind(this),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { Autowired, Injectable } from '@opensumi/di';
|
||||
|
||||
import { BaseApplyService } from '../../base-apply.service';
|
||||
|
||||
/**
|
||||
* TODO: 代码块改动版本号,次数,流式工具调用?
|
||||
* 基础文件编辑处理类
|
||||
* 用于处理代码改动的应用、保存等操作
|
||||
*/
|
||||
@Injectable()
|
||||
export class EditFileHandler {
|
||||
@Autowired(BaseApplyService)
|
||||
private applyService: BaseApplyService;
|
||||
|
||||
async handler(relativePath: string, updateContent: string, instructions?: string) {
|
||||
// TODO: ignore file
|
||||
this.applyService.registerCodeBlock(relativePath, updateContent);
|
||||
const blockData = await this.applyService.apply(relativePath, updateContent, instructions);
|
||||
return blockData;
|
||||
}
|
||||
}
|
|
@ -12,6 +12,11 @@ export class FileHandler {
|
|||
private static readonly MAX_CHARS = 1e5;
|
||||
private static readonly NEWLINE = '\n';
|
||||
|
||||
private fileResultMap: Map<
|
||||
string,
|
||||
{ content: string; startLineOneIndexed: number; endLineOneIndexedInclusive: number }
|
||||
> = new Map();
|
||||
|
||||
@Autowired(IEditorDocumentModelService)
|
||||
protected modelService: IEditorDocumentModelService;
|
||||
|
||||
|
@ -141,7 +146,14 @@ export class FileHandler {
|
|||
didShortenCharRange = true;
|
||||
selectedContent = this.trimContent(selectedContent, FileHandler.MAX_CHARS);
|
||||
}
|
||||
|
||||
// 文件的浏览窗口需要记录,应用的时候需要用
|
||||
if (didShortenLineRange) {
|
||||
this.fileResultMap.set(fileParams.relativeWorkspacePath, {
|
||||
content: selectedContent,
|
||||
startLineOneIndexed: adjustedStart,
|
||||
endLineOneIndexedInclusive: adjustedEnd,
|
||||
});
|
||||
}
|
||||
return {
|
||||
contents: selectedContent,
|
||||
didDowngradeToLineRange: shouldForceLimitLines,
|
||||
|
@ -171,4 +183,10 @@ export class FileHandler {
|
|||
modelReference?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
getFileReadResult(
|
||||
relativeWorkspacePath: string,
|
||||
): { content: string; startLineOneIndexed: number; endLineOneIndexedInclusive: number } | undefined {
|
||||
return this.fileResultMap.get(relativeWorkspacePath);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export function generateCodeBlockId(composerId: string, messageId: string): string {
|
||||
return `${composerId}:${messageId}`;
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
import { Autowired } from '@opensumi/di';
|
||||
import { Domain } from '@opensumi/ide-core-common';
|
||||
|
@ -33,16 +32,15 @@ export class ListDirTool implements MCPServerContribution {
|
|||
getToolDefinition(): MCPToolDefinition {
|
||||
return {
|
||||
name: 'list_dir',
|
||||
label: 'List Directory',
|
||||
description:
|
||||
'List the contents of a directory. The quick tool to use for discovery, before using more targeted tools like semantic search or file reading. Useful to try to understand the file structure before diving deeper into specific files. Can be used to explore the codebase.',
|
||||
inputSchema: zodToJsonSchema(inputSchema),
|
||||
inputSchema,
|
||||
handler: this.handler.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
private async handler(args: z.infer<typeof inputSchema>, logger: MCPLogger) {
|
||||
// TODO: 应该添加统一的 validate 逻辑
|
||||
args = inputSchema.parse(args);
|
||||
const result = await this.listDirHandler.handler(args);
|
||||
return {
|
||||
content: [
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
import { Autowired } from '@opensumi/di';
|
||||
import { Domain } from '@opensumi/ide-core-common';
|
||||
|
@ -37,6 +36,7 @@ export class ReadFileTool implements MCPServerContribution {
|
|||
getToolDefinition(): MCPToolDefinition {
|
||||
return {
|
||||
name: 'read_file',
|
||||
label: 'Read File',
|
||||
description: `Read the contents of a file (and the outline).
|
||||
|
||||
When using this tool to gather information, it's your responsibility to ensure you have the COMPLETE context. Each time you call this command you should:
|
||||
|
@ -48,14 +48,12 @@ When using this tool to gather information, it's your responsibility to ensure y
|
|||
If reading a range of lines is not enough, you may choose to read the entire file.
|
||||
Reading entire files is often wasteful and slow, especially for large files (i.e. more than a few hundred lines). So you should use this option sparingly.
|
||||
Reading the entire file is not allowed in most cases. You are only allowed to read the entire file if it has been edited or manually attached to the conversation by the user.`,
|
||||
inputSchema: zodToJsonSchema(inputSchema),
|
||||
inputSchema,
|
||||
handler: this.handler.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
private async handler(args: z.infer<typeof inputSchema>, logger: MCPLogger) {
|
||||
// TODO: 应该添加统一的 validate 逻辑
|
||||
args = inputSchema.parse(args);
|
||||
const result = await this.fileHandler.readFile(args);
|
||||
return {
|
||||
content: [
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
import { Autowired, Injectable } from '@opensumi/di';
|
||||
import { Domain } from '@opensumi/ide-core-common';
|
||||
|
@ -25,13 +24,13 @@ export class ReplaceOpenEditorFileTool implements MCPServerContribution {
|
|||
name: 'replace_open_in_editor_file_text',
|
||||
description:
|
||||
'Replaces the entire content of the currently active file in the IDE editor with specified new text. ' +
|
||||
'Use this tool when you need to completely overwrite the current file\'s content. ' +
|
||||
"Use this tool when you need to completely overwrite the current file's content. " +
|
||||
'Requires a text parameter containing the new content. ' +
|
||||
'Returns one of three possible responses: ' +
|
||||
'"ok" if the file content was successfully replaced, ' +
|
||||
'"no file open" if no editor is active, ' +
|
||||
'"unknown error" if the operation fails.',
|
||||
inputSchema: zodToJsonSchema(inputSchema),
|
||||
inputSchema,
|
||||
handler: this.handler.bind(this),
|
||||
};
|
||||
}
|
||||
|
@ -60,10 +59,12 @@ export class ReplaceOpenEditorFileTool implements MCPServerContribution {
|
|||
const fullRange = model.getFullModelRange();
|
||||
|
||||
// Execute the replacement
|
||||
editor.monacoEditor.executeEdits('mcp.tool.replace-file', [{
|
||||
range: fullRange,
|
||||
text: args.text,
|
||||
}]);
|
||||
editor.monacoEditor.executeEdits('mcp.tool.replace-file', [
|
||||
{
|
||||
range: fullRange,
|
||||
text: args.text,
|
||||
},
|
||||
]);
|
||||
|
||||
logger.appendLine('Successfully replaced file content');
|
||||
return {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
import { Autowired, Injectable } from '@opensumi/di';
|
||||
import { Domain } from '@opensumi/ide-core-common';
|
||||
|
@ -34,7 +33,7 @@ export class ReplaceOpenEditorFileByDiffPreviewerTool implements MCPServerContri
|
|||
'"ok" if the file content was successfully replaced, ' +
|
||||
'"no file open" if no editor is active, ' +
|
||||
'"unknown error" if the operation fails.',
|
||||
inputSchema: zodToJsonSchema(inputSchema),
|
||||
inputSchema,
|
||||
handler: this.handler.bind(this),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
import { Autowired } from '@opensumi/di';
|
||||
import { AppConfig } from '@opensumi/ide-core-browser';
|
||||
|
@ -48,7 +47,7 @@ export class RunTerminalCommandTool implements MCPServerContribution {
|
|||
name: 'run_terminal_cmd',
|
||||
description:
|
||||
"PROPOSE a command to run on behalf of the user.\nIf you have this tool, note that you DO have the ability to run commands directly on the USER's system.\n\nAdhere to these rules:\n1. Based on the contents of the conversation, you will be told if you are in the same shell as a previous step or a new shell.\n2. If in a new shell, you should `cd` to the right directory and do necessary setup in addition to running the command.\n3. If in the same shell, the state will persist, no need to do things like `cd` to the same directory.\n4. For ANY commands that would use a pager, you should append ` | cat` to the command (or whatever is appropriate). You MUST do this for: git, less, head, tail, more, etc.\n5. For commands that are long running/expected to run indefinitely until interruption, please run them in the background. To run jobs in the background, set `is_background` to true rather than changing the details of the command.\n6. Dont include any newlines in the command.",
|
||||
inputSchema: zodToJsonSchema(inputSchema),
|
||||
inputSchema,
|
||||
handler: this.handler.bind(this),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -122,5 +122,10 @@ export const aiNativePreferenceSchema: PreferenceSchema = {
|
|||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
[AINativeSettingSectionsId.SystemPrompt]: {
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: localize('preference.ai.native.chat.system.prompt.description'),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import { ZodSchema } from 'zod';
|
||||
|
||||
import { AIActionItem } from '@opensumi/ide-core-browser/lib/components/ai-native/index';
|
||||
import {
|
||||
|
@ -349,8 +350,9 @@ export interface MCPLogger {
|
|||
|
||||
export interface MCPToolDefinition {
|
||||
name: string;
|
||||
label?: string;
|
||||
description: string;
|
||||
inputSchema: any; // JSON Schema
|
||||
inputSchema: ZodSchema<any>; // JSON Schema
|
||||
handler: (
|
||||
args: any,
|
||||
logger: MCPLogger,
|
||||
|
@ -360,9 +362,21 @@ export interface MCPToolDefinition {
|
|||
}>;
|
||||
}
|
||||
|
||||
export interface IMCPServerToolComponentProps {
|
||||
state?: 'streaming-start' | 'streaming' | 'complete' | 'result';
|
||||
args?: Record<string, any>;
|
||||
result?: any;
|
||||
index?: number;
|
||||
messageId?: string;
|
||||
toolCallId?: string;
|
||||
}
|
||||
|
||||
export interface IMCPServerRegistry {
|
||||
registerMCPTool(tool: MCPToolDefinition): void;
|
||||
getMCPTools(): MCPToolDefinition[];
|
||||
getMCPTool(name: string): MCPToolDefinition | undefined;
|
||||
registerToolComponent(name: string, component: React.FC<IMCPServerToolComponentProps>): void;
|
||||
getToolComponent(name: string): React.FC<IMCPServerToolComponentProps> | undefined;
|
||||
callMCPTool(
|
||||
name: string,
|
||||
args: any,
|
||||
|
|
|
@ -69,6 +69,10 @@ export class InlineStreamDiffHandler extends Disposable implements IInlineDiffPr
|
|||
|
||||
public livePreviewDiffDecorationModel: LivePreviewDiffDecorationModel;
|
||||
|
||||
public get uri() {
|
||||
return this.originalModel.uri;
|
||||
}
|
||||
|
||||
constructor(private readonly monacoEditor: ICodeEditor) {
|
||||
super();
|
||||
|
||||
|
|
|
@ -65,6 +65,10 @@ export interface IPartialEditEvent {
|
|||
* 已添加行数
|
||||
*/
|
||||
totalAddedLinesCount: number;
|
||||
/**
|
||||
* 已采纳的个数
|
||||
*/
|
||||
acceptPartialEditCount: number;
|
||||
/**
|
||||
* 已删除行数
|
||||
*/
|
||||
|
|
|
@ -426,6 +426,7 @@ export class LivePreviewDiffDecorationModel extends Disposable {
|
|||
uri: this.model.uri,
|
||||
totalPartialEditCount: this.partialEditWidgetList.length,
|
||||
resolvedPartialEditCount: this.partialEditWidgetList.filter((w) => w.isHidden).length,
|
||||
acceptPartialEditCount: this.partialEditWidgetList.filter((w) => w.isAccepted).length,
|
||||
currentPartialEdit: {
|
||||
addedLinesCount,
|
||||
deletedLinesCount,
|
||||
|
|
|
@ -179,6 +179,7 @@ export interface IChatAgentMetadata {
|
|||
fullName?: string;
|
||||
icon?: Uri;
|
||||
iconDark?: Uri;
|
||||
systemPrompt?: string;
|
||||
}
|
||||
|
||||
export interface IChatAgentRequest {
|
||||
|
|
|
@ -48,3 +48,5 @@ export const extractCodeBlocks = (content: string): string => {
|
|||
|
||||
return newContents.join('\n');
|
||||
};
|
||||
|
||||
export const getToolName = (toolName: string, serverName = 'sumi-builtin') => `mcp_${serverName}_${toolName}`;
|
||||
|
|
|
@ -52,6 +52,11 @@ export abstract class BaseLanguageModel {
|
|||
allFunctions,
|
||||
chatReadableStream,
|
||||
options.history || [],
|
||||
options.modelId,
|
||||
options.temperature,
|
||||
options.topP,
|
||||
options.topK,
|
||||
options.providerOptions,
|
||||
cancellationToken,
|
||||
);
|
||||
}
|
||||
|
@ -65,7 +70,7 @@ export abstract class BaseLanguageModel {
|
|||
});
|
||||
}
|
||||
|
||||
protected abstract getModelIdentifier(provider: any): any;
|
||||
protected abstract getModelIdentifier(provider: any, modelId?: string): any;
|
||||
|
||||
protected async handleStreamingRequest(
|
||||
provider: any,
|
||||
|
@ -73,6 +78,11 @@ export abstract class BaseLanguageModel {
|
|||
tools: ToolRequest[],
|
||||
chatReadableStream: ChatReadableStream,
|
||||
history: IChatMessage[] = [],
|
||||
modelId?: string,
|
||||
temperature?: number,
|
||||
topP?: number,
|
||||
topK?: number,
|
||||
providerOptions?: Record<string, any>,
|
||||
cancellationToken?: CancellationToken,
|
||||
): Promise<any> {
|
||||
try {
|
||||
|
@ -92,15 +102,18 @@ export abstract class BaseLanguageModel {
|
|||
})),
|
||||
{ role: 'user', content: request },
|
||||
];
|
||||
|
||||
const stream = await streamText({
|
||||
model: this.getModelIdentifier(provider),
|
||||
const stream = streamText({
|
||||
model: this.getModelIdentifier(provider, modelId),
|
||||
maxTokens: 4096,
|
||||
tools: aiTools,
|
||||
messages,
|
||||
abortSignal: abortController.signal,
|
||||
experimental_toolCallStreaming: true,
|
||||
maxSteps: 12,
|
||||
temperature,
|
||||
topP: topP || 0.8,
|
||||
topK: topK || 1,
|
||||
providerOptions,
|
||||
});
|
||||
|
||||
for await (const chunk of stream.fullStream) {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { ILogger } from '@opensumi/ide-core-common';
|
|||
|
||||
import { MCPServerDescription, MCPServerManager, MCPTool } from '../common/mcp-server-manager';
|
||||
import { IToolInvocationRegistryManager, ToolRequest } from '../common/tool-invocation-registry';
|
||||
import { getToolName } from '../common/utils';
|
||||
|
||||
import { BuiltinMCPServer } from './mcp/sumi-mcp-server';
|
||||
import { IMCPServer, MCPServerImpl } from './mcp-server';
|
||||
|
@ -62,7 +63,7 @@ export class MCPServerManagerImpl implements MCPServerManager {
|
|||
}
|
||||
|
||||
private convertToToolRequest(tool: MCPTool, serverName: string): ToolRequest {
|
||||
const id = `mcp_${serverName}_${tool.name}`;
|
||||
const id = getToolName(tool.name, serverName);
|
||||
|
||||
return {
|
||||
id,
|
||||
|
|
|
@ -19,7 +19,7 @@ export class OpenAIModel extends BaseLanguageModel {
|
|||
});
|
||||
}
|
||||
|
||||
protected getModelIdentifier(provider: OpenAIProvider) {
|
||||
return provider('qwen-max');
|
||||
protected getModelIdentifier(provider: OpenAIProvider, modelId?: string) {
|
||||
return provider(modelId || 'qwen-max');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,11 @@ export enum AINativeSettingSectionsId {
|
|||
*/
|
||||
MCPServers = 'ai.native.mcp.servers',
|
||||
CodeEditsTyping = 'ai.native.codeEdits.typing',
|
||||
|
||||
/**
|
||||
* System prompt
|
||||
*/
|
||||
SystemPrompt = 'ai.native.chat.system.prompt',
|
||||
}
|
||||
export const AI_NATIVE_SETTING_GROUP_ID = 'AI-Native';
|
||||
export const AI_NATIVE_SETTING_GROUP_TITLE = 'AI Native';
|
||||
|
|
|
@ -173,8 +173,15 @@ export interface IAIBackServiceOption {
|
|||
tools?: any[];
|
||||
clientId?: string;
|
||||
apiKey?: string;
|
||||
/** 模型提供商,如 openai, anthropic, deepseek */
|
||||
model?: string;
|
||||
/** 模型ID,如 gpt-4o-mini, claude-3-5-sonnet-20240620 */
|
||||
modelId?: string;
|
||||
baseURL?: string;
|
||||
temperature?: number;
|
||||
topP?: number;
|
||||
topK?: number;
|
||||
providerOptions?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1514,6 +1514,7 @@ export const localizationBundle = {
|
|||
'preference.ai.native.codeEdits.lineChange':
|
||||
'Whether to trigger intelligent rewriting when the cursor line number changes',
|
||||
'preference.ai.native.codeEdits.typing': 'Whether to trigger intelligent rewriting when the content changes',
|
||||
'preference.ai.native.chat.system.prompt': 'Default Chat System Prompt',
|
||||
// #endregion AI Native
|
||||
|
||||
// #endregion merge editor
|
||||
|
|
|
@ -1280,6 +1280,7 @@ export const localizationBundle = {
|
|||
'preference.ai.native.codeEdits.lintErrors': '是否在发生 Lint Error 时触发智能改写',
|
||||
'preference.ai.native.codeEdits.lineChange': '是否在光标行号发生变化时触发智能改写',
|
||||
'preference.ai.native.codeEdits.typing': '是否在内容发生变化时触发智能改写',
|
||||
'preference.ai.native.chat.system.prompt': '默认聊天系统提示词',
|
||||
// #endregion AI Native
|
||||
|
||||
'webview.webviewTagUnavailable': '非 Electron 环境不支持 webview 标签,请使用 iframe 标签',
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
import { Autowired, Injectable } from '@opensumi/di';
|
||||
import { ChatResponseModel } from '@opensumi/ide-ai-native/lib/browser/chat/chat-model';
|
||||
import { ChatProxyService } from '@opensumi/ide-ai-native/lib/browser/chat/chat-proxy.service';
|
||||
import { BaseApplyService } from '@opensumi/ide-ai-native/lib/browser/mcp/base-apply.service';
|
||||
import {
|
||||
AIBackSerivcePath,
|
||||
AINativeSettingSectionsId,
|
||||
ChatMessageRole,
|
||||
IAIBackService,
|
||||
IApplicationService,
|
||||
IChatProgress,
|
||||
IMarker,
|
||||
PreferenceService,
|
||||
URI,
|
||||
path,
|
||||
uuid,
|
||||
} from '@opensumi/ide-core-browser';
|
||||
import { IEditorDocumentModelService } from '@opensumi/ide-editor/lib/browser';
|
||||
import { listenReadable } from '@opensumi/ide-utils/lib/stream';
|
||||
import { Range } from '@opensumi/monaco-editor-core';
|
||||
|
||||
@Injectable()
|
||||
export class ApplyService extends BaseApplyService {
|
||||
@Autowired(IEditorDocumentModelService)
|
||||
private readonly modelService: IEditorDocumentModelService;
|
||||
|
||||
@Autowired(IApplicationService)
|
||||
private readonly applicationService: IApplicationService;
|
||||
|
||||
@Autowired(AIBackSerivcePath)
|
||||
private readonly aiBackService: IAIBackService;
|
||||
|
||||
@Autowired(PreferenceService)
|
||||
private readonly preferenceService: PreferenceService;
|
||||
|
||||
protected async doApply(
|
||||
relativePath: string,
|
||||
newContent: string,
|
||||
instructions?: string,
|
||||
): Promise<{ diff: string; diagnosticInfos: IMarker[] } | undefined> {
|
||||
let fileReadResult = this.fileHandler.getFileReadResult(relativePath);
|
||||
const uri = new URI(path.join(this.appConfig.workspaceDir, relativePath));
|
||||
const modelReference = await this.modelService.createModelReference(uri);
|
||||
const fileContent = modelReference.instance.getMonacoModel().getValue();
|
||||
if (!fileReadResult) {
|
||||
fileReadResult = {
|
||||
content: fileContent,
|
||||
startLineOneIndexed: 1,
|
||||
endLineOneIndexedInclusive: fileContent.split('\n').length,
|
||||
};
|
||||
}
|
||||
const apiKey = this.preferenceService.get<string>(AINativeSettingSectionsId.OpenaiApiKey, '');
|
||||
const baseURL = this.preferenceService.get<string>(AINativeSettingSectionsId.OpenaiBaseURL, '');
|
||||
const stream = await this.aiBackService.requestStream(
|
||||
`Merge all changes from the <update> snippet into the <code> below.
|
||||
- Preserve the code's structure, order, comments, and indentation exactly.
|
||||
- Output only the updated code, enclosed within <updated-code> and </updated-code> tags.
|
||||
- Do not include any additional text, explanations, placeholders, ellipses, or code fences.
|
||||
|
||||
<code>${fileReadResult.content}</code>
|
||||
|
||||
<update>${newContent}</update>
|
||||
|
||||
Provide the complete updated code.
|
||||
<updated-code>`,
|
||||
{
|
||||
model: 'openai',
|
||||
modelId: 'qwen-turbo',
|
||||
baseURL,
|
||||
apiKey,
|
||||
clientId: this.applicationService.clientId,
|
||||
history: [
|
||||
{
|
||||
id: 'system',
|
||||
order: 0,
|
||||
role: ChatMessageRole.System,
|
||||
content:
|
||||
'You are a coding assistant that helps merge code updates, ensuring every modification is fully integrated.',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const chatResponse = new ChatResponseModel(
|
||||
uuid(),
|
||||
this.chatInternalService.sessionModel,
|
||||
ChatProxyService.AGENT_ID,
|
||||
);
|
||||
listenReadable<IChatProgress>(stream, {
|
||||
onData: (data) => {
|
||||
chatResponse.updateContent(data, true);
|
||||
},
|
||||
onEnd: () => {
|
||||
chatResponse.complete();
|
||||
},
|
||||
onError: (error) => {
|
||||
chatResponse.setErrorDetails({
|
||||
message: error.message,
|
||||
});
|
||||
chatResponse.cancel();
|
||||
},
|
||||
});
|
||||
const openResult = await this.editorService.open(URI.file(this.appConfig.workspaceDir + '/' + relativePath));
|
||||
if (!openResult) {
|
||||
throw new Error('Failed to open editor');
|
||||
}
|
||||
|
||||
return await new Promise<{ diff: string; diagnosticInfos: IMarker[] } | undefined>((resolve, reject) => {
|
||||
chatResponse.onDidChange(async () => {
|
||||
if (chatResponse.isComplete) {
|
||||
if (chatResponse.errorDetails) {
|
||||
reject(new Error(chatResponse.errorDetails.message));
|
||||
}
|
||||
// Set the new content
|
||||
const newContent = chatResponse.responseText.match(/<updated-code>([\s\S]*?)<\/updated-code>/)?.[1] || '';
|
||||
if (!newContent) {
|
||||
reject(new Error('No updated code found'));
|
||||
}
|
||||
const applyResult = await this.renderApplyResult(
|
||||
relativePath,
|
||||
newContent,
|
||||
new Range(fileReadResult.startLineOneIndexed, 0, fileReadResult.endLineOneIndexedInclusive, 0),
|
||||
);
|
||||
resolve(applyResult);
|
||||
} else if (chatResponse.isCanceled) {
|
||||
reject(new Error('Apply cancelled: ' + chatResponse.errorDetails?.message));
|
||||
}
|
||||
});
|
||||
});
|
||||
// TODO: 诊断信息+迭代
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
import { Injectable, Provider } from '@opensumi/di';
|
||||
import { BaseApplyService } from '@opensumi/ide-ai-native/lib/browser/mcp/base-apply.service';
|
||||
import { BrowserModule } from '@opensumi/ide-core-browser';
|
||||
import { AbstractNodeExtProcessService } from '@opensumi/ide-extension/lib/common/extension.service';
|
||||
|
||||
import { AINativeContribution } from './ai-native/ai-native.contribution';
|
||||
import { ApplyService } from './ai-native/apply.service';
|
||||
import { DebugConfigurationContribution } from './debug-configuration.contribution';
|
||||
import { EditorEmptyComponentContribution } from './editor-empty-component.contribution';
|
||||
import { MenuBarContribution } from './menu-bar/menu-bar.contribution';
|
||||
|
@ -22,5 +24,9 @@ export class SampleModule extends BrowserModule {
|
|||
useClass: OverrideExtensionNodeService,
|
||||
override: true,
|
||||
},
|
||||
{
|
||||
token: BaseApplyService,
|
||||
useClass: ApplyService,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -109,6 +109,7 @@
|
|||
"@opensumi/ide-testing": "workspace:*",
|
||||
"@opensumi/ide-theme": "workspace:*",
|
||||
"@opensumi/ide-toolbar": "workspace:*",
|
||||
"@opensumi/ide-utils": "workspace:*",
|
||||
"@opensumi/ide-variable": "workspace:*",
|
||||
"@opensumi/ide-webview": "workspace:*",
|
||||
"@opensumi/ide-workspace": "workspace:*",
|
||||
|
|
|
@ -212,9 +212,9 @@ exports.createWebpackConfig = function (dir, entry, extraConfig) {
|
|||
'process.env.OTHER_EXTENSION_DIR': JSON.stringify(path.join(__dirname, '../../../other')),
|
||||
'process.env.EXTENSION_WORKER_HOST': JSON.stringify(
|
||||
process.env.EXTENSION_WORKER_HOST ||
|
||||
`http://${HOST}:8080/assets` +
|
||||
withSlash +
|
||||
path.resolve(__dirname, '../../../packages/extension/lib/worker-host.js'),
|
||||
`http://${HOST}:8080/assets` +
|
||||
withSlash +
|
||||
path.resolve(__dirname, '../../../packages/extension/lib/worker-host.js'),
|
||||
),
|
||||
'process.env.WS_PATH': JSON.stringify(process.env.WS_PATH || `ws://${HOST}:8000`),
|
||||
'process.env.WEBVIEW_HOST': JSON.stringify(process.env.WEBVIEW_HOST || HOST),
|
||||
|
@ -222,18 +222,18 @@ exports.createWebpackConfig = function (dir, entry, extraConfig) {
|
|||
'process.env.HOST': JSON.stringify(process.env.HOST),
|
||||
}),
|
||||
!process.env.SKIP_TS_CHECKER &&
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
typescript: {
|
||||
diagnosticOptions: {
|
||||
syntactic: true,
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
typescript: {
|
||||
diagnosticOptions: {
|
||||
syntactic: true,
|
||||
},
|
||||
configFile: tsConfigPath,
|
||||
},
|
||||
configFile: tsConfigPath,
|
||||
},
|
||||
issue: {
|
||||
include: (issue) => issue.file.includes('src/packages/'),
|
||||
exclude: (issue) => issue.file.includes('__test__'),
|
||||
},
|
||||
}),
|
||||
issue: {
|
||||
include: (issue) => issue.file.includes('src/packages/'),
|
||||
exclude: (issue) => issue.file.includes('__test__'),
|
||||
},
|
||||
}),
|
||||
new NodePolyfillPlugin({
|
||||
includeAliases: ['process', 'Buffer'],
|
||||
}),
|
||||
|
|
70
yarn.lock
70
yarn.lock
|
@ -247,21 +247,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@anthropic-ai/sdk@npm:^0.36.3":
|
||||
version: 0.36.3
|
||||
resolution: "@anthropic-ai/sdk@npm:0.36.3"
|
||||
dependencies:
|
||||
"@types/node": "npm:^18.11.18"
|
||||
"@types/node-fetch": "npm:^2.6.4"
|
||||
abort-controller: "npm:^3.0.0"
|
||||
agentkeepalive: "npm:^4.2.1"
|
||||
form-data-encoder: "npm:1.7.2"
|
||||
formdata-node: "npm:^4.3.2"
|
||||
node-fetch: "npm:^2.6.7"
|
||||
checksum: 10/fb6f2551c4dd090b32ca613b71c99f35dd4886bb2344fb9c0cdfb9562273ebe60dc9534e621dc892d71d26b7ef9eb6c55c6c201488077e2cd20cb4cafd8a3a03
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ast-grep/napi-darwin-arm64@npm:0.17.1":
|
||||
version: 0.17.1
|
||||
resolution: "@ast-grep/napi-darwin-arm64@npm:0.17.1"
|
||||
|
@ -3382,7 +3367,6 @@ __metadata:
|
|||
"@ai-sdk/anthropic": "npm:^1.1.6"
|
||||
"@ai-sdk/deepseek": "npm:^0.1.8"
|
||||
"@ai-sdk/openai": "npm:^1.1.9"
|
||||
"@anthropic-ai/sdk": "npm:^0.36.3"
|
||||
"@modelcontextprotocol/sdk": "npm:^1.3.1"
|
||||
"@opensumi/ide-addons": "workspace:*"
|
||||
"@opensumi/ide-components": "workspace:*"
|
||||
|
@ -3395,10 +3379,8 @@ __metadata:
|
|||
"@opensumi/ide-editor": "workspace:*"
|
||||
"@opensumi/ide-file-search": "workspace:*"
|
||||
"@opensumi/ide-file-service": "workspace:*"
|
||||
"@opensumi/ide-file-tree-next": "workspace:*"
|
||||
"@opensumi/ide-main-layout": "workspace:*"
|
||||
"@opensumi/ide-markers": "workspace:*"
|
||||
"@opensumi/ide-menu-bar": "workspace:*"
|
||||
"@opensumi/ide-monaco": "workspace:*"
|
||||
"@opensumi/ide-overlay": "workspace:*"
|
||||
"@opensumi/ide-preferences": "workspace:*"
|
||||
|
@ -3409,6 +3391,7 @@ __metadata:
|
|||
"@xterm/xterm": "npm:5.5.0"
|
||||
ai: "npm:^4.1.21"
|
||||
ansi-regex: "npm:^2.0.0"
|
||||
diff: "npm:^7.0.0"
|
||||
dom-align: "npm:^1.7.0"
|
||||
rc-collapse: "npm:^4.0.0"
|
||||
react-chat-elements: "npm:^12.0.10"
|
||||
|
@ -4252,6 +4235,7 @@ __metadata:
|
|||
"@opensumi/ide-testing": "workspace:*"
|
||||
"@opensumi/ide-theme": "workspace:*"
|
||||
"@opensumi/ide-toolbar": "workspace:*"
|
||||
"@opensumi/ide-utils": "workspace:*"
|
||||
"@opensumi/ide-variable": "workspace:*"
|
||||
"@opensumi/ide-webview": "workspace:*"
|
||||
"@opensumi/ide-workspace": "workspace:*"
|
||||
|
@ -5778,16 +5762,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node-fetch@npm:^2.6.4":
|
||||
version: 2.6.12
|
||||
resolution: "@types/node-fetch@npm:2.6.12"
|
||||
dependencies:
|
||||
"@types/node": "npm:*"
|
||||
form-data: "npm:^4.0.0"
|
||||
checksum: 10/8107c479da83a3114fcbfa882eba95ee5175cccb5e4dd53f737a96f2559ae6262f662176b8457c1656de09ec393cc7b20a266c077e4bfb21e929976e1cf4d0f9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node-forge@npm:^1.3.0":
|
||||
version: 1.3.11
|
||||
resolution: "@types/node-forge@npm:1.3.11"
|
||||
|
@ -5820,15 +5794,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^18.11.18":
|
||||
version: 18.19.68
|
||||
resolution: "@types/node@npm:18.19.68"
|
||||
dependencies:
|
||||
undici-types: "npm:~5.26.4"
|
||||
checksum: 10/024a4a8eeca21c0d1eaa575036dbc44528eae180821de71b77868ddc24d18032b988582046db4f7ea2643970a5169d790e1884153472145de07d629bc2ce2ec6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^22.7.6":
|
||||
version: 22.7.6
|
||||
resolution: "@types/node@npm:22.7.6"
|
||||
|
@ -12198,13 +12163,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"form-data-encoder@npm:1.7.2":
|
||||
version: 1.7.2
|
||||
resolution: "form-data-encoder@npm:1.7.2"
|
||||
checksum: 10/227bf2cea083284411fd67472ccc22f5cb354ca92c00690e11ff5ed942d993c13ac99dea365046306200f8bd71e1a7858d2d99e236de694b806b1f374a4ee341
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"form-data@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "form-data@npm:4.0.0"
|
||||
|
@ -12216,16 +12174,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"formdata-node@npm:^4.3.2":
|
||||
version: 4.4.1
|
||||
resolution: "formdata-node@npm:4.4.1"
|
||||
dependencies:
|
||||
node-domexception: "npm:1.0.0"
|
||||
web-streams-polyfill: "npm:4.0.0-beta.3"
|
||||
checksum: 10/29622f75533107c1bbcbe31fda683e6a55859af7f48ec354a9800591ce7947ed84cd3ef2b2fcb812047a884f17a1bac75ce098ffc17e23402cd373e49c1cd335
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"forwarded@npm:0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "forwarded@npm:0.2.0"
|
||||
|
@ -17508,13 +17456,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-domexception@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "node-domexception@npm:1.0.0"
|
||||
checksum: 10/e332522f242348c511640c25a6fc7da4f30e09e580c70c6b13cb0be83c78c3e71c8d4665af2527e869fc96848924a4316ae7ec9014c091e2156f41739d4fa233
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-fetch@npm:2.6.7":
|
||||
version: 2.6.7
|
||||
resolution: "node-fetch@npm:2.6.7"
|
||||
|
@ -24844,13 +24785,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"web-streams-polyfill@npm:4.0.0-beta.3":
|
||||
version: 4.0.0-beta.3
|
||||
resolution: "web-streams-polyfill@npm:4.0.0-beta.3"
|
||||
checksum: 10/dcdef67de57d83008f9dc330662b65ba4497315555dd0e4e7bcacb132ffdf8a830eaab8f74ad40a4a44f542461f51223f406e2a446ece1cc29927859b1405853
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"web-tree-sitter@npm:0.22.6":
|
||||
version: 0.22.6
|
||||
resolution: "web-tree-sitter@npm:0.22.6"
|
||||
|
|
Loading…
Reference in New Issue