@opensumi/ide-ai-native 3.8.3-next-1741752385.0 → 3.8.3-next-1741763277.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/chat/chat-model.d.ts +2 -2
- package/lib/browser/chat/chat-model.d.ts.map +1 -1
- package/lib/browser/chat/chat-model.js +18 -1
- package/lib/browser/chat/chat-model.js.map +1 -1
- package/lib/browser/components/ChatReply.d.ts.map +1 -1
- package/lib/browser/components/ChatReply.js +35 -17
- package/lib/browser/components/ChatReply.js.map +1 -1
- package/lib/browser/components/ChatThinking.js +1 -1
- package/lib/browser/components/ChatThinking.js.map +1 -1
- package/lib/browser/components/WelcomeMsg.js +1 -1
- package/lib/browser/components/WelcomeMsg.js.map +1 -1
- package/lib/browser/components/components.module.less +24 -0
- package/lib/browser/contrib/inline-completions/prompt/matcher.js +2 -2
- package/lib/browser/contrib/inline-completions/prompt/similarSnippets.d.ts +1 -1
- package/lib/browser/contrib/inline-completions/prompt/similarSnippets.js +2 -2
- package/lib/browser/contrib/intelligent-completions/view/default.d.ts.map +1 -1
- package/lib/browser/contrib/intelligent-completions/view/default.js.map +1 -1
- package/lib/browser/mcp/config/components/mcp-config.view.d.ts.map +1 -1
- package/lib/browser/mcp/config/components/mcp-config.view.js +28 -18
- package/lib/browser/mcp/config/components/mcp-config.view.js.map +1 -1
- package/lib/browser/mcp/config/components/mcp-server-form.d.ts.map +1 -1
- package/lib/browser/mcp/config/components/mcp-server-form.js +33 -25
- package/lib/browser/mcp/config/components/mcp-server-form.js.map +1 -1
- package/lib/browser/mcp/mcp-server.feature.registry.js +1 -1
- package/lib/browser/mcp/mcp-server.feature.registry.js.map +1 -1
- package/lib/browser/mcp/tools/components/ExpandableFileList.d.ts.map +1 -1
- package/lib/browser/mcp/tools/components/ExpandableFileList.js +3 -1
- package/lib/browser/mcp/tools/components/ExpandableFileList.js.map +1 -1
- package/lib/browser/mcp/tools/components/Terminal.d.ts.map +1 -1
- package/lib/browser/mcp/tools/components/Terminal.js +6 -5
- package/lib/browser/mcp/tools/components/Terminal.js.map +1 -1
- package/lib/browser/mcp/tools/components/computeAnsiLogString.d.ts +4 -0
- package/lib/browser/mcp/tools/components/computeAnsiLogString.d.ts.map +1 -0
- package/lib/browser/mcp/tools/components/computeAnsiLogString.js +22 -0
- package/lib/browser/mcp/tools/components/computeAnsiLogString.js.map +1 -0
- package/lib/browser/mcp/tools/components/filterEraseMultipleLine.d.ts +18 -0
- package/lib/browser/mcp/tools/components/filterEraseMultipleLine.d.ts.map +1 -0
- package/lib/browser/mcp/tools/components/filterEraseMultipleLine.js +69 -0
- package/lib/browser/mcp/tools/components/filterEraseMultipleLine.js.map +1 -0
- package/lib/browser/mcp/tools/components/index.module.less +8 -5
- package/lib/browser/mcp/tools/createNewFileWithText.d.ts.map +1 -1
- package/lib/browser/mcp/tools/createNewFileWithText.js +1 -0
- package/lib/browser/mcp/tools/createNewFileWithText.js.map +1 -1
- package/lib/browser/mcp/tools/editFile.d.ts.map +1 -1
- package/lib/browser/mcp/tools/editFile.js +1 -0
- package/lib/browser/mcp/tools/editFile.js.map +1 -1
- package/lib/browser/mcp/tools/fileSearch.d.ts.map +1 -1
- package/lib/browser/mcp/tools/fileSearch.js +1 -0
- package/lib/browser/mcp/tools/fileSearch.js.map +1 -1
- package/lib/browser/mcp/tools/getDiagnosticsByPath.d.ts.map +1 -1
- package/lib/browser/mcp/tools/getDiagnosticsByPath.js +2 -1
- package/lib/browser/mcp/tools/getDiagnosticsByPath.js.map +1 -1
- package/lib/browser/mcp/tools/getOpenEditorFileDiagnostics.d.ts.map +1 -1
- package/lib/browser/mcp/tools/getOpenEditorFileDiagnostics.js +2 -0
- package/lib/browser/mcp/tools/getOpenEditorFileDiagnostics.js.map +1 -1
- package/lib/browser/mcp/tools/grepSearch.d.ts.map +1 -1
- package/lib/browser/mcp/tools/grepSearch.js +1 -0
- package/lib/browser/mcp/tools/grepSearch.js.map +1 -1
- package/lib/browser/mcp/tools/listDir.d.ts.map +1 -1
- package/lib/browser/mcp/tools/listDir.js +1 -0
- package/lib/browser/mcp/tools/listDir.js.map +1 -1
- package/lib/browser/mcp/tools/readFile.d.ts.map +1 -1
- package/lib/browser/mcp/tools/readFile.js +1 -0
- package/lib/browser/mcp/tools/readFile.js.map +1 -1
- package/lib/browser/mcp/tools/runTerminalCmd.d.ts.map +1 -1
- package/lib/browser/mcp/tools/runTerminalCmd.js +1 -0
- package/lib/browser/mcp/tools/runTerminalCmd.js.map +1 -1
- package/lib/browser/types.d.ts +1 -0
- package/lib/browser/types.d.ts.map +1 -1
- package/lib/browser/widget/inline-diff/inline-diff-manager.d.ts.map +1 -1
- package/lib/browser/widget/inline-diff/inline-diff-manager.js.map +1 -1
- package/lib/browser/widget/inline-stream-diff/live-preview.decoration.d.ts.map +1 -1
- package/lib/browser/widget/inline-stream-diff/live-preview.decoration.js +10 -5
- package/lib/browser/widget/inline-stream-diff/live-preview.decoration.js.map +1 -1
- package/lib/common/prompts/context-prompt-provider.d.ts.map +1 -1
- package/lib/common/prompts/context-prompt-provider.js +4 -2
- package/lib/common/prompts/context-prompt-provider.js.map +1 -1
- package/lib/node/base-language-model.d.ts.map +1 -1
- package/lib/node/base-language-model.js +6 -0
- package/lib/node/base-language-model.js.map +1 -1
- package/package.json +24 -23
- package/src/browser/chat/chat-model.ts +19 -2
- package/src/browser/components/ChatReply.tsx +61 -18
- package/src/browser/components/ChatThinking.tsx +1 -1
- package/src/browser/components/WelcomeMsg.tsx +1 -1
- package/src/browser/components/components.module.less +24 -0
- package/src/browser/contrib/inline-completions/prompt/matcher.ts +2 -2
- package/src/browser/contrib/inline-completions/prompt/similarSnippets.ts +2 -2
- package/src/browser/contrib/intelligent-completions/view/default.ts +0 -1
- package/src/browser/mcp/config/components/mcp-config.view.tsx +23 -12
- package/src/browser/mcp/config/components/mcp-server-form.tsx +68 -54
- package/src/browser/mcp/mcp-server.feature.registry.ts +1 -1
- package/src/browser/mcp/tools/components/ExpandableFileList.tsx +4 -1
- package/src/browser/mcp/tools/components/Terminal.tsx +4 -6
- package/src/browser/mcp/tools/components/computeAnsiLogString.ts +24 -0
- package/src/browser/mcp/tools/components/filterEraseMultipleLine.ts +71 -0
- package/src/browser/mcp/tools/components/index.module.less +8 -5
- package/src/browser/mcp/tools/createNewFileWithText.ts +1 -0
- package/src/browser/mcp/tools/editFile.ts +1 -0
- package/src/browser/mcp/tools/fileSearch.ts +1 -0
- package/src/browser/mcp/tools/getDiagnosticsByPath.ts +2 -1
- package/src/browser/mcp/tools/getOpenEditorFileDiagnostics.ts +2 -0
- package/src/browser/mcp/tools/grepSearch.ts +1 -0
- package/src/browser/mcp/tools/listDir.ts +1 -0
- package/src/browser/mcp/tools/readFile.ts +1 -0
- package/src/browser/mcp/tools/runTerminalCmd.ts +1 -0
- package/src/browser/types.ts +1 -0
- package/src/browser/widget/inline-diff/inline-diff-manager.tsx +0 -1
- package/src/browser/widget/inline-stream-diff/live-preview.decoration.tsx +9 -5
- package/src/common/prompts/context-prompt-provider.ts +6 -2
- package/src/node/base-language-model.ts +5 -0
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
IChatComponent,
|
|
7
7
|
IChatMarkdownContent,
|
|
8
8
|
IChatProgress,
|
|
9
|
+
IChatReasoning,
|
|
9
10
|
IChatToolContent,
|
|
10
11
|
IChatTreeData,
|
|
11
12
|
uuid,
|
|
@@ -33,7 +34,8 @@ export type IChatProgressResponseContent =
|
|
|
33
34
|
| IChatAsyncContent
|
|
34
35
|
| IChatTreeData
|
|
35
36
|
| IChatComponent
|
|
36
|
-
| IChatToolContent
|
|
37
|
+
| IChatToolContent
|
|
38
|
+
| IChatReasoning;
|
|
37
39
|
|
|
38
40
|
export class ChatResponseModel extends Disposable {
|
|
39
41
|
#responseParts: IChatProgressResponseContent[] = [];
|
|
@@ -131,6 +133,18 @@ export class ChatResponseModel extends Disposable {
|
|
|
131
133
|
};
|
|
132
134
|
}
|
|
133
135
|
|
|
136
|
+
this.#updateResponseText();
|
|
137
|
+
} else if (progress.kind === 'reasoning') {
|
|
138
|
+
const lastResponsePart = this.#responseParts[responsePartLength];
|
|
139
|
+
if (!lastResponsePart || lastResponsePart.kind !== 'reasoning') {
|
|
140
|
+
// 去掉开头的 <think> 标签
|
|
141
|
+
this.#responseParts.push({ content: progress.content.replace(/^<think>/, ''), kind: 'reasoning' });
|
|
142
|
+
} else {
|
|
143
|
+
this.#responseParts[responsePartLength] = {
|
|
144
|
+
content: lastResponsePart.content + progress.content,
|
|
145
|
+
kind: 'reasoning',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
134
148
|
this.#updateResponseText();
|
|
135
149
|
} else if (progress.kind === 'asyncContent') {
|
|
136
150
|
// Add a new resolving part
|
|
@@ -181,6 +195,9 @@ export class ChatResponseModel extends Disposable {
|
|
|
181
195
|
if (part.kind === 'toolCall') {
|
|
182
196
|
return part.content.function.name;
|
|
183
197
|
}
|
|
198
|
+
if (part.kind === 'reasoning') {
|
|
199
|
+
return '';
|
|
200
|
+
}
|
|
184
201
|
return part.content.value;
|
|
185
202
|
})
|
|
186
203
|
.join('\n\n');
|
|
@@ -387,7 +404,7 @@ export class ChatModel extends Disposable implements IChatModel {
|
|
|
387
404
|
|
|
388
405
|
const { kind } = progress;
|
|
389
406
|
|
|
390
|
-
const basicKind = ['content', 'markdownContent', 'asyncContent', 'treeData', 'component', 'toolCall'];
|
|
407
|
+
const basicKind = ['content', 'markdownContent', 'asyncContent', 'treeData', 'component', 'toolCall', 'reasoning'];
|
|
391
408
|
|
|
392
409
|
if (basicKind.includes(kind)) {
|
|
393
410
|
request.response.updateContent(progress, quiet);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import cls from 'classnames';
|
|
1
2
|
import React, {
|
|
2
3
|
Fragment,
|
|
3
4
|
ReactNode,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
IChatResponseProgressFileTreeData,
|
|
41
42
|
IChatToolContent,
|
|
42
43
|
URI,
|
|
44
|
+
localize,
|
|
43
45
|
} from '@opensumi/ide-core-common';
|
|
44
46
|
import { IIconService } from '@opensumi/ide-theme';
|
|
45
47
|
import { IMarkdownString, MarkdownString } from '@opensumi/monaco-editor-core/esm/vs/base/common/htmlContent';
|
|
@@ -224,6 +226,28 @@ export const ChatReply = (props: IChatReplyProps) => {
|
|
|
224
226
|
const chatApiService = useInjectable<ChatService>(ChatServiceToken);
|
|
225
227
|
const chatAgentService = useInjectable<IChatAgentService>(IChatAgentService);
|
|
226
228
|
const chatRenderRegistry = useInjectable<ChatRenderRegistry>(ChatRenderRegistryToken);
|
|
229
|
+
const [collapseThinkingIndexSet, setCollapseThinkingIndexSet] = useState<Set<number>>(
|
|
230
|
+
!request.response.isComplete
|
|
231
|
+
? new Set()
|
|
232
|
+
: new Set(
|
|
233
|
+
request.response.responseContents
|
|
234
|
+
.map((item, index) => (item.kind === 'reasoning' ? index : -1))
|
|
235
|
+
.filter((item) => item !== -1),
|
|
236
|
+
),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
if (request.response.isComplete) {
|
|
241
|
+
setCollapseThinkingIndexSet(
|
|
242
|
+
new Set(
|
|
243
|
+
request.response.responseContents
|
|
244
|
+
.map((item, index) => (item.kind === 'reasoning' ? index : -1))
|
|
245
|
+
.filter((item) => item !== -1),
|
|
246
|
+
),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}, [request.response.isComplete]);
|
|
250
|
+
|
|
227
251
|
useEffect(() => {
|
|
228
252
|
const disposableCollection = new DisposableCollection();
|
|
229
253
|
|
|
@@ -263,23 +287,6 @@ export const ChatReply = (props: IChatReplyProps) => {
|
|
|
263
287
|
onRegenerate?.();
|
|
264
288
|
}, [onRegenerate]);
|
|
265
289
|
|
|
266
|
-
const onStop = () => {
|
|
267
|
-
if (onDone) {
|
|
268
|
-
onDone();
|
|
269
|
-
}
|
|
270
|
-
aiReporter.end(relationId, {
|
|
271
|
-
assistantMessage: request.response.responseText,
|
|
272
|
-
replytime: Date.now() - startTime,
|
|
273
|
-
success: false,
|
|
274
|
-
isStop: true,
|
|
275
|
-
command,
|
|
276
|
-
agentId,
|
|
277
|
-
messageId: msgId,
|
|
278
|
-
sessionId: aiChatService.sessionModel.sessionId,
|
|
279
|
-
});
|
|
280
|
-
aiChatService.cancelRequest();
|
|
281
|
-
};
|
|
282
|
-
|
|
283
290
|
const renderMarkdown = useCallback(
|
|
284
291
|
(markdown: IMarkdownString) => {
|
|
285
292
|
if (chatRenderRegistry.chatAIRoleRender) {
|
|
@@ -313,12 +320,48 @@ export const ChatReply = (props: IChatReplyProps) => {
|
|
|
313
320
|
node = <ComponentRender component={item.component} value={item.value} messageId={msgId} />;
|
|
314
321
|
} else if (item.kind === 'toolCall') {
|
|
315
322
|
node = <ToolCallRender toolCall={item.content} messageId={msgId} />;
|
|
323
|
+
} else if (item.kind === 'reasoning') {
|
|
324
|
+
// 思考中必然为最后一条
|
|
325
|
+
const isThinking = index === request.response.responseContents.length - 1 && !request.response.isComplete;
|
|
326
|
+
node = (
|
|
327
|
+
<div className={cls(styles.reasoning, { [styles.thinking]: isThinking })}>
|
|
328
|
+
<Button
|
|
329
|
+
size='small'
|
|
330
|
+
type='secondary'
|
|
331
|
+
className={styles.thinking}
|
|
332
|
+
onClick={() => {
|
|
333
|
+
if (isThinking) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (collapseThinkingIndexSet.has(index)) {
|
|
337
|
+
collapseThinkingIndexSet.delete(index);
|
|
338
|
+
} else {
|
|
339
|
+
collapseThinkingIndexSet.add(index);
|
|
340
|
+
}
|
|
341
|
+
setCollapseThinkingIndexSet(new Set(collapseThinkingIndexSet));
|
|
342
|
+
}}
|
|
343
|
+
>
|
|
344
|
+
<Icon iconClass='codicon codicon-sparkle' />
|
|
345
|
+
{localize('aiNative.chat.thinking')}
|
|
346
|
+
{isThinking ? (
|
|
347
|
+
<Loading />
|
|
348
|
+
) : collapseThinkingIndexSet.has(index) ? (
|
|
349
|
+
<Icon iconClass='codicon codicon-chevron-right' />
|
|
350
|
+
) : (
|
|
351
|
+
<Icon iconClass='codicon codicon-chevron-down' />
|
|
352
|
+
)}
|
|
353
|
+
</Button>
|
|
354
|
+
{!collapseThinkingIndexSet.has(index) ? (
|
|
355
|
+
<div className={styles.reasoning_content}>{renderMarkdown(new MarkdownString(item.content))}</div>
|
|
356
|
+
) : null}
|
|
357
|
+
</div>
|
|
358
|
+
);
|
|
316
359
|
} else {
|
|
317
360
|
node = renderMarkdown(item.content);
|
|
318
361
|
}
|
|
319
362
|
return <Fragment key={`${item.kind}-${index}`}>{node}</Fragment>;
|
|
320
363
|
}),
|
|
321
|
-
[request.response.responseContents],
|
|
364
|
+
[request.response.responseContents, collapseThinkingIndexSet],
|
|
322
365
|
);
|
|
323
366
|
|
|
324
367
|
const followupNode = React.useMemo(() => {
|
|
@@ -33,7 +33,7 @@ export const ChatThinking = (props: ITinkingProps) => {
|
|
|
33
33
|
);
|
|
34
34
|
|
|
35
35
|
const renderContent = useCallback(() => {
|
|
36
|
-
if (!children
|
|
36
|
+
if (!children) {
|
|
37
37
|
if (CustomThinkingRender) {
|
|
38
38
|
return <CustomThinkingRender thinkingText={thinkingText} />;
|
|
39
39
|
}
|
|
@@ -79,7 +79,7 @@ export const WelcomeMessage = () => {
|
|
|
79
79
|
return (
|
|
80
80
|
<div className={styles.chat_welcome_head}>
|
|
81
81
|
<div className={styles.chat_container_des}>
|
|
82
|
-
{isMarkdownString(welcomeMessage) ? <ChatMarkdown markdown={welcomeMessage} /> : welcomeMessage}
|
|
82
|
+
{isMarkdownString(welcomeMessage) ? <ChatMarkdown key='welcome' markdown={welcomeMessage} /> : welcomeMessage}
|
|
83
83
|
</div>
|
|
84
84
|
<div className={styles.chat_container_content}>
|
|
85
85
|
{allSampleQuestions.map((data: any, index) => {
|
|
@@ -541,3 +541,27 @@
|
|
|
541
541
|
}
|
|
542
542
|
}
|
|
543
543
|
}
|
|
544
|
+
|
|
545
|
+
.reasoning {
|
|
546
|
+
.thinking {
|
|
547
|
+
display: flex;
|
|
548
|
+
align-items: center;
|
|
549
|
+
gap: 4px;
|
|
550
|
+
margin-bottom: 4px;
|
|
551
|
+
transition: color 0.2s ease-in-out;
|
|
552
|
+
:global {
|
|
553
|
+
.codicon {
|
|
554
|
+
color: inherit;
|
|
555
|
+
}
|
|
556
|
+
.codicon-sparkle {
|
|
557
|
+
margin-right: -1px;
|
|
558
|
+
font-size: 14px;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
.reasoning_content {
|
|
563
|
+
padding-left: 12px;
|
|
564
|
+
border-left: 2px solid var(--descriptionForeground);
|
|
565
|
+
color: var(--descriptionForeground);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
@@ -244,8 +244,8 @@ export abstract class WindowedMatcher {
|
|
|
244
244
|
tokens.slice(startLine, endLine).forEach((token) => token.forEach((word) => size.add(word)));
|
|
245
245
|
cache.push(size);
|
|
246
246
|
}
|
|
247
|
-
const
|
|
248
|
-
const score = this.similarityScore(
|
|
247
|
+
const target = cache[index];
|
|
248
|
+
const score = this.similarityScore(target, this.referenceTokens);
|
|
249
249
|
snippets.push({
|
|
250
250
|
score,
|
|
251
251
|
startLine,
|
|
@@ -15,9 +15,9 @@ import {
|
|
|
15
15
|
import { MAX_NEIGHBOR_AGGREGATE_LENGTH } from './const';
|
|
16
16
|
import { FixedWindowSizeJaccardMatcher } from './jaccardMatcher';
|
|
17
17
|
|
|
18
|
-
export const getOpenedTabFileList = (
|
|
18
|
+
export const getOpenedTabFileList = (documents: IEditorDocumentModel[]) => {
|
|
19
19
|
// 过滤超大文档
|
|
20
|
-
const recentFiles =
|
|
20
|
+
const recentFiles = documents.filter((document) => isDocumentValid(document));
|
|
21
21
|
return recentFiles;
|
|
22
22
|
};
|
|
23
23
|
|
|
@@ -4,8 +4,8 @@ import React, { useCallback } from 'react';
|
|
|
4
4
|
import { Badge } from '@opensumi/ide-components';
|
|
5
5
|
import { AINativeSettingSectionsId, ILogger, useInjectable } from '@opensumi/ide-core-browser';
|
|
6
6
|
import { PreferenceService } from '@opensumi/ide-core-browser/lib/preferences';
|
|
7
|
-
import { localize } from '@opensumi/ide-core-common';
|
|
8
|
-
import { IMessageService } from '@opensumi/ide-overlay
|
|
7
|
+
import { PreferenceScope, localize } from '@opensumi/ide-core-common';
|
|
8
|
+
import { IMessageService } from '@opensumi/ide-overlay';
|
|
9
9
|
|
|
10
10
|
import { BUILTIN_MCP_SERVER_NAME, ISumiMCPServerBackend, SumiMCPServerProxyServicePath } from '../../../../common';
|
|
11
11
|
import { MCPServerDescription } from '../../../../common/mcp-server-manager';
|
|
@@ -18,13 +18,13 @@ import { MCPServerForm, MCPServerFormData } from './mcp-server-form';
|
|
|
18
18
|
export const MCPConfigView: React.FC = () => {
|
|
19
19
|
const mcpServerProxyService = useInjectable<MCPServerProxyService>(MCPServerProxyService);
|
|
20
20
|
const preferenceService = useInjectable<PreferenceService>(PreferenceService);
|
|
21
|
+
const messageService = useInjectable<IMessageService>(IMessageService);
|
|
21
22
|
const sumiMCPServerBackendProxy = useInjectable<ISumiMCPServerBackend>(SumiMCPServerProxyServicePath);
|
|
22
23
|
const logger = useInjectable<ILogger>(ILogger);
|
|
23
|
-
const messageService = useInjectable<IMessageService>(IMessageService);
|
|
24
24
|
const [servers, setServers] = React.useState<MCPServer[]>([]);
|
|
25
25
|
const [formVisible, setFormVisible] = React.useState(false);
|
|
26
26
|
const [editingServer, setEditingServer] = React.useState<MCPServerFormData | undefined>();
|
|
27
|
-
|
|
27
|
+
const [loadingServer, setLoadingServer] = React.useState<string | undefined>();
|
|
28
28
|
const loadServers = useCallback(async () => {
|
|
29
29
|
const allServers = await mcpServerProxyService.$getServers();
|
|
30
30
|
setServers(allServers);
|
|
@@ -44,6 +44,7 @@ export const MCPConfigView: React.FC = () => {
|
|
|
44
44
|
const handleServerControl = useCallback(
|
|
45
45
|
async (serverName: string, start: boolean) => {
|
|
46
46
|
try {
|
|
47
|
+
setLoadingServer(serverName);
|
|
47
48
|
if (start) {
|
|
48
49
|
await mcpServerProxyService.$startServer(serverName);
|
|
49
50
|
} else {
|
|
@@ -87,12 +88,14 @@ export const MCPConfigView: React.FC = () => {
|
|
|
87
88
|
});
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
await preferenceService.set(AINativeSettingSectionsId.MCPServers, updatedServers);
|
|
91
|
+
await preferenceService.set(AINativeSettingSectionsId.MCPServers, updatedServers, PreferenceScope.User);
|
|
91
92
|
await loadServers();
|
|
93
|
+
setLoadingServer(undefined);
|
|
92
94
|
} catch (error) {
|
|
93
95
|
const msg = error.message || error;
|
|
94
96
|
logger.error(`Failed to ${start ? 'start' : 'stop'} server ${serverName}:`, error);
|
|
95
|
-
messageService.error(
|
|
97
|
+
messageService.error(error.message);
|
|
98
|
+
setLoadingServer(undefined);
|
|
96
99
|
}
|
|
97
100
|
},
|
|
98
101
|
[mcpServerProxyService, preferenceService, sumiMCPServerBackendProxy, loadServers],
|
|
@@ -121,7 +124,7 @@ export const MCPConfigView: React.FC = () => {
|
|
|
121
124
|
const servers = preferenceService.get<MCPServerFormData[]>(AINativeSettingSectionsId.MCPServers, []);
|
|
122
125
|
const updatedServers = servers.filter((s) => s.name !== serverName);
|
|
123
126
|
sumiMCPServerBackendProxy.removeServer(serverName);
|
|
124
|
-
await preferenceService.set(AINativeSettingSectionsId.MCPServers, updatedServers);
|
|
127
|
+
await preferenceService.set(AINativeSettingSectionsId.MCPServers, updatedServers, PreferenceScope.User);
|
|
125
128
|
await loadServers();
|
|
126
129
|
},
|
|
127
130
|
[editingServer, formVisible],
|
|
@@ -140,7 +143,7 @@ export const MCPConfigView: React.FC = () => {
|
|
|
140
143
|
setServers(servers as MCPServer[]);
|
|
141
144
|
setFormVisible(false);
|
|
142
145
|
await sumiMCPServerBackendProxy.addOrUpdateServer(data as MCPServerDescription);
|
|
143
|
-
await preferenceService.set(AINativeSettingSectionsId.MCPServers, servers);
|
|
146
|
+
await preferenceService.set(AINativeSettingSectionsId.MCPServers, servers, PreferenceScope.User);
|
|
144
147
|
await loadServers();
|
|
145
148
|
},
|
|
146
149
|
[servers, formVisible, loadServers],
|
|
@@ -164,10 +167,10 @@ export const MCPConfigView: React.FC = () => {
|
|
|
164
167
|
<div className={styles.header}>
|
|
165
168
|
<div>
|
|
166
169
|
<h2 className={styles.title}>MCP Servers</h2>
|
|
167
|
-
<p className={styles.description}>
|
|
170
|
+
<p className={styles.description}>{localize('ai.native.mcp.manage.connections')}</p>
|
|
168
171
|
</div>
|
|
169
172
|
<button className={styles.addButton} onClick={handleAddServer}>
|
|
170
|
-
+
|
|
173
|
+
+ {localize('ai.native.mcp.addMCPServer.title')}
|
|
171
174
|
</button>
|
|
172
175
|
</div>
|
|
173
176
|
<div className={styles.serversList}>
|
|
@@ -188,7 +191,15 @@ export const MCPConfigView: React.FC = () => {
|
|
|
188
191
|
title={server.isStarted ? 'Stop' : 'Start'}
|
|
189
192
|
onClick={() => handleServerControl(server.name, !server.isStarted)}
|
|
190
193
|
>
|
|
191
|
-
<i
|
|
194
|
+
<i
|
|
195
|
+
className={`codicon ${
|
|
196
|
+
loadingServer === server.name
|
|
197
|
+
? 'codicon-loading kt-icon-loading'
|
|
198
|
+
: server.isStarted
|
|
199
|
+
? 'codicon-debug-stop'
|
|
200
|
+
: 'codicon-debug-start'
|
|
201
|
+
}`}
|
|
202
|
+
/>
|
|
192
203
|
</button>
|
|
193
204
|
{server.name !== BUILTIN_MCP_SERVER_NAME && (
|
|
194
205
|
<button className={styles.iconButton} title='Delete' onClick={() => handleDeleteServer(server.name)}>
|
|
@@ -201,7 +212,7 @@ export const MCPConfigView: React.FC = () => {
|
|
|
201
212
|
<div className={styles.detailRow}>
|
|
202
213
|
<span className={styles.detailLabel}>Status:</span>
|
|
203
214
|
<span className={`${styles.serverStatus} ${server.isStarted ? styles.running : styles.stopped}`}>
|
|
204
|
-
{server.isStarted ? '
|
|
215
|
+
{server.isStarted ? localize('ai.native.mcp.running') : localize('ai.native.mcp.stopped')}
|
|
205
216
|
</span>
|
|
206
217
|
</div>
|
|
207
218
|
{server.type && (
|
|
@@ -67,35 +67,75 @@ export const MCPServerForm: FC<Props> = ({ visible, initialData, onSave, onCance
|
|
|
67
67
|
);
|
|
68
68
|
}, [initialData]);
|
|
69
69
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
70
|
+
const validateForm = useCallback(
|
|
71
|
+
(formData: MCPServerFormData) => {
|
|
72
|
+
if (formData.name.trim() === '') {
|
|
73
|
+
messageService.error(localize('ai.native.mcp.name.isRequired'));
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (
|
|
77
|
+
!initialData &&
|
|
78
|
+
existingServers.some((server) => server.name.toLocaleLowerCase() === formData.name.toLocaleLowerCase())
|
|
79
|
+
) {
|
|
80
|
+
messageService.error(formatLocalize('ai.native.mcp.serverNameExists', formData.name));
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (formData.type === MCP_SERVER_TYPE.SSE) {
|
|
84
|
+
const isServerHostValid = formData.serverHost?.trim() !== '';
|
|
85
|
+
if (!isServerHostValid) {
|
|
86
|
+
messageService.error(localize('ai.native.mcp.serverHost.isRequired'));
|
|
87
|
+
}
|
|
88
|
+
return isServerHostValid;
|
|
89
|
+
}
|
|
90
|
+
const isCommandValid = formData.command?.trim() !== '';
|
|
91
|
+
if (!isCommandValid) {
|
|
92
|
+
messageService.error(localize('ai.native.mcp.command.isRequired'));
|
|
93
|
+
}
|
|
94
|
+
return isCommandValid;
|
|
95
|
+
},
|
|
96
|
+
[existingServers, initialData],
|
|
97
|
+
);
|
|
96
98
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
const handleSubmit = useCallback(
|
|
100
|
+
(e: FormEvent) => {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
const isValid = validateForm(formData);
|
|
103
|
+
if (!isValid) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const form = {
|
|
107
|
+
...formData,
|
|
108
|
+
};
|
|
109
|
+
if (formData.type === MCP_SERVER_TYPE.SSE) {
|
|
110
|
+
form.serverHost = form.serverHost?.trim();
|
|
111
|
+
} else {
|
|
112
|
+
const args = argsText.split(' ').filter(Boolean);
|
|
113
|
+
const env = envText
|
|
114
|
+
.split('\n')
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.reduce((acc, line) => {
|
|
117
|
+
const [key, value] = line.split('=');
|
|
118
|
+
if (key && value) {
|
|
119
|
+
acc[key.trim()] = value.trim();
|
|
120
|
+
}
|
|
121
|
+
return acc;
|
|
122
|
+
}, {} as Record<string, string>);
|
|
123
|
+
form.args = args;
|
|
124
|
+
form.env = env;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
setFormData({
|
|
128
|
+
...formData,
|
|
129
|
+
command: '',
|
|
130
|
+
serverHost: '',
|
|
131
|
+
args: [],
|
|
132
|
+
env: {},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
onSave(form);
|
|
136
|
+
},
|
|
137
|
+
[formData, argsText, envText, onSave, validateForm],
|
|
138
|
+
);
|
|
99
139
|
|
|
100
140
|
const handleCommandChange = useCallback(
|
|
101
141
|
(e: ChangeEvent<HTMLInputElement>) => {
|
|
@@ -183,32 +223,6 @@ export const MCPServerForm: FC<Props> = ({ visible, initialData, onSave, onCance
|
|
|
183
223
|
}
|
|
184
224
|
}, [formData, argsText, envText]);
|
|
185
225
|
|
|
186
|
-
const validateForm = useCallback(
|
|
187
|
-
(formData: MCPServerFormData) => {
|
|
188
|
-
if (formData.name.trim() === '') {
|
|
189
|
-
messageService.error(localize('ai.native.mcp.name.isRequired'));
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
192
|
-
if (existingServers.some((server) => server.name.toLocaleLowerCase() === formData.name.toLocaleLowerCase())) {
|
|
193
|
-
messageService.error(formatLocalize('ai.native.mcp.serverNameExists', formData.name));
|
|
194
|
-
return false;
|
|
195
|
-
}
|
|
196
|
-
if (formData.type === MCP_SERVER_TYPE.SSE) {
|
|
197
|
-
const isServerHostValid = formData.serverHost?.trim() !== '';
|
|
198
|
-
if (!isServerHostValid) {
|
|
199
|
-
messageService.error(localize('ai.native.mcp.serverHost.isRequired'));
|
|
200
|
-
}
|
|
201
|
-
return isServerHostValid;
|
|
202
|
-
}
|
|
203
|
-
const isCommandValid = formData.command?.trim() !== '';
|
|
204
|
-
if (!isCommandValid) {
|
|
205
|
-
messageService.error(localize('ai.native.mcp.command.isRequired'));
|
|
206
|
-
}
|
|
207
|
-
return isCommandValid;
|
|
208
|
-
},
|
|
209
|
-
[existingServers],
|
|
210
|
-
);
|
|
211
|
-
|
|
212
226
|
return (
|
|
213
227
|
<Modal
|
|
214
228
|
title={initialData ? localize('ai.native.mcp.editMCPServer.title') : localize('ai.native.mcp.addMCPServer.title')}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
|
|
3
|
+
import { Icon } from '@opensumi/ide-components/lib/icon/icon';
|
|
3
4
|
import { CommandService, LabelService, URI, path, useInjectable } from '@opensumi/ide-core-browser';
|
|
4
5
|
import { WorkbenchEditorService } from '@opensumi/ide-editor';
|
|
5
6
|
import { IWorkspaceService } from '@opensumi/ide-workspace';
|
|
@@ -110,7 +111,9 @@ const ExpandableFileList: React.FC<ExpandableFileListProps> = ({
|
|
|
110
111
|
return (
|
|
111
112
|
<div className={styles.container}>
|
|
112
113
|
<div className={styles.header} onClick={() => setIsExpanded(!isExpanded)}>
|
|
113
|
-
<span style={{ transform: `rotate(${isExpanded ? '90deg' : '0deg'})
|
|
114
|
+
<span style={{ transform: `rotate(${isExpanded ? '90deg' : '0deg'})`, display: 'flex' }}>
|
|
115
|
+
<Icon iconClass={'codicon codicon-chevron-right'} />
|
|
116
|
+
</span>
|
|
114
117
|
<span>
|
|
115
118
|
{headerText} · {fileList.length} files
|
|
116
119
|
</span>
|
|
@@ -3,11 +3,11 @@ import React, { memo, useCallback, useMemo, useState } from 'react';
|
|
|
3
3
|
import { useInjectable } from '@opensumi/ide-core-browser';
|
|
4
4
|
import { Button, Icon } from '@opensumi/ide-core-browser/lib/components';
|
|
5
5
|
import { localize } from '@opensumi/ide-core-common';
|
|
6
|
-
import { stripAnsi } from '@opensumi/ide-utils/lib/ansi';
|
|
7
6
|
|
|
8
7
|
import { IMCPServerToolComponentProps } from '../../../types';
|
|
9
8
|
import { RunCommandHandler } from '../handlers/RunCommand';
|
|
10
9
|
|
|
10
|
+
import { computeAnsiLogString } from './computeAnsiLogString';
|
|
11
11
|
import styles from './index.module.less';
|
|
12
12
|
|
|
13
13
|
function getResult(raw: string) {
|
|
@@ -63,19 +63,17 @@ export const TerminalToolComponent = memo((props: IMCPServerToolComponentProps)
|
|
|
63
63
|
<>
|
|
64
64
|
<div className={styles.command_title}>
|
|
65
65
|
<Icon icon='terminal' />
|
|
66
|
-
<span>{localize('ai.native.mcp.terminal.command')}
|
|
66
|
+
<span>{localize('ai.native.mcp.terminal.command')}:</span>
|
|
67
67
|
</div>
|
|
68
68
|
<p className={styles.command_content}>
|
|
69
69
|
<code>$ {args.command}</code>
|
|
70
70
|
</p>
|
|
71
71
|
</>
|
|
72
72
|
)}
|
|
73
|
-
<div className={styles.command_title}>
|
|
74
|
-
<span>{localize('ai.native.mcp.terminal.output')}</span>
|
|
75
|
-
</div>
|
|
76
73
|
{output ? (
|
|
77
74
|
<div className={styles.command_content}>
|
|
78
|
-
<
|
|
75
|
+
<Icon icon='output' />
|
|
76
|
+
<code dangerouslySetInnerHTML={{ __html: computeAnsiLogString(output.text || '') }} />
|
|
79
77
|
</div>
|
|
80
78
|
) : (
|
|
81
79
|
''
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import AnsiUp from 'ansi_up';
|
|
2
|
+
|
|
3
|
+
import filterEraseMultipleLine from './filterEraseMultipleLine';
|
|
4
|
+
|
|
5
|
+
type LogContent = string;
|
|
6
|
+
|
|
7
|
+
const ansiUp = new AnsiUp();
|
|
8
|
+
|
|
9
|
+
export function computeAnsiLogString(logs: LogContent, enableEraseLineFilter = true, hideEmptyLine = false): string {
|
|
10
|
+
const splittedLogs = logs.split('\n');
|
|
11
|
+
// 处理清空上行逻辑
|
|
12
|
+
// 上移 cursor + 清空整行
|
|
13
|
+
let filteredLogs = enableEraseLineFilter ? filterEraseMultipleLine(splittedLogs) : splittedLogs;
|
|
14
|
+
if (hideEmptyLine) {
|
|
15
|
+
filteredLogs = filteredLogs.map((line) => line.replace('\r', '')).filter((line) => !!line);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const htmlLogLines = filteredLogs.map((line) => {
|
|
19
|
+
const htmlLog = ansiUp.ansi_to_html(line);
|
|
20
|
+
|
|
21
|
+
return htmlLog;
|
|
22
|
+
});
|
|
23
|
+
return htmlLogLines.join('\n');
|
|
24
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export const ESC = '\u001B[';
|
|
2
|
+
export const eraseLine = ESC + '2K';
|
|
3
|
+
export const eraseEndLine = ESC + 'K';
|
|
4
|
+
|
|
5
|
+
export const cursorUp = (count = 1) => ESC + count + 'A';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 处理过滤清空上行,清空本行逻辑。
|
|
9
|
+
*
|
|
10
|
+
* 关于清空上 n 行:
|
|
11
|
+
* 一般在日志中,出现覆盖上行的情况,ascii 编码为 2K [1A 2K ...] 1G 的样式。 如 \u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\r\n,代表清空上两行。
|
|
12
|
+
* 其中,2K 代表清空整行,1A 代表光标上移,配合下一个 2K 则最终效果为清空上行,而 1G 是移动光标到本行开始(位置 1)。
|
|
13
|
+
* 在日志过滤过程中,可以只处理 1A 2K 这个序列,遇到后把该日志的上一行删掉即可。
|
|
14
|
+
*
|
|
15
|
+
* 关于清空本行,按顺序执行:
|
|
16
|
+
* 1. 在当前行没有 Cursor 操作符(如上移时),匹配最后一个 [2K (清空本行)或 \r[K(指针回 0,再清空本行到末尾,相当于清空本行),只输出 [2K 后的内容,
|
|
17
|
+
* 2. 在当前行没有 Cursor 操作符时且有多个 \r (carriage return charactor,移动光标到行首)时,reduce 按 \r \x1b[G 或 \x1b[1G 切分的片断,不段用后一 part 的部分从头覆盖得出结果。
|
|
18
|
+
*/
|
|
19
|
+
export default function filterEraseMultipleLine(logs: string[]) {
|
|
20
|
+
// 上移 cursor + 清空整行
|
|
21
|
+
const eraseLastLine = cursorUp(1) + eraseLine;
|
|
22
|
+
const eraseCurrentLine = eraseLine;
|
|
23
|
+
const eraseCurrentLine2 = `\r${eraseEndLine}`;
|
|
24
|
+
|
|
25
|
+
const moveCursorToLeftRegStrs = ['\\r', '\\u001b\\[G', '\\u001b\\[1G'];
|
|
26
|
+
const moveCursorToLeftRegStr = new RegExp(`${moveCursorToLeftRegStrs.join('|')}`);
|
|
27
|
+
|
|
28
|
+
const filteredLogs = logs.reduce((acc: string[], nowLine) => {
|
|
29
|
+
// 当前清空上行搜索指针
|
|
30
|
+
let pos = 0;
|
|
31
|
+
const step = eraseLastLine.length;
|
|
32
|
+
|
|
33
|
+
while (true) {
|
|
34
|
+
pos = nowLine.indexOf(eraseLastLine, pos);
|
|
35
|
+
// 出现清空上行
|
|
36
|
+
if (pos >= 0) {
|
|
37
|
+
pos += step;
|
|
38
|
+
acc.pop();
|
|
39
|
+
} else {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 对单行日志的重写做处理
|
|
45
|
+
// 简单处理,不去解析真正的 Cursor 所在行,否则逻辑过于麻烦
|
|
46
|
+
// 处理 [2K
|
|
47
|
+
let lastErasePos = nowLine.lastIndexOf(eraseCurrentLine);
|
|
48
|
+
if (lastErasePos < 0) {
|
|
49
|
+
// 处理 \r[K
|
|
50
|
+
lastErasePos = nowLine.lastIndexOf(eraseCurrentLine2);
|
|
51
|
+
}
|
|
52
|
+
if (lastErasePos > 0) {
|
|
53
|
+
// 从后向前搜索最后一个清行操作
|
|
54
|
+
nowLine = nowLine.slice(lastErasePos);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 处理多 \r 情况,当 \r 连续时,切分出的空字段无用,过滤掉
|
|
58
|
+
const carriageRewrites = nowLine.split(moveCursorToLeftRegStr).filter((part) => !!part);
|
|
59
|
+
if (carriageRewrites.length > 1) {
|
|
60
|
+
nowLine = carriageRewrites.reduce((nextNowLine, nowPart) => {
|
|
61
|
+
const leftPart = nextNowLine.slice(nowPart.length);
|
|
62
|
+
return nowPart + leftPart;
|
|
63
|
+
}, '');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
acc.push(nowLine);
|
|
67
|
+
return acc;
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
return filteredLogs;
|
|
71
|
+
}
|