mirror of https://github.com/opensumi/core
feat: support new chat mention input for MCP model (#4466)
* feat: support more powerfull input * chore: update iconfont * chore: update iconfont resource * style: support better style * chore: update icons * chore: update iconfont * chore: update css source * chore: update kaitian-icon resource * style: improve mention tag style * chore: update iconfont * feat: support more beautiful chat input * feat: support mention input placeholder * chore: update placeholder * style: improve empty search style * chore: update iconfont resource * chore: update iconfont resource * feat: support chat mention input UX * feat: support chat file and folder context * chore: add minWidth for chat selection * feat: add onSelectionChange prop to MentionInput component * feat: improve prompt * feat: support file and folder reference display * feat: support file and folder navigator * style: add global icon margin style in components * style: improve Chat UI * chore: remove useless code * chore: update styles * feat: improve folder location on file explorer * feat: optimize file tree explorer activation handling * chore: update mention input style * fix: folder and history style * style: improve chat history style * style: improve markdown style * fix: reval file or folder on explorer * refactor: improve file match logic
This commit is contained in:
parent
b1eeb004c8
commit
82ddf9af7a
|
@ -19,6 +19,7 @@
|
||||||
"build:all": "yarn build:webview-prebuilt && yarn run build && yarn run build:worker-host && yarn run build:ext-host && yarn build:watcher-host && yarn run build:components && yarn build:monaco-worker",
|
"build:all": "yarn build:webview-prebuilt && yarn run build && yarn run build:worker-host && yarn run build:ext-host && yarn build:watcher-host && yarn run build:components && yarn build:monaco-worker",
|
||||||
"build:cli-engine": "cd tools/cli-engine && yarn run build",
|
"build:cli-engine": "cd tools/cli-engine && yarn run build",
|
||||||
"build:components": "cd packages/components && yarn run build:dist",
|
"build:components": "cd packages/components && yarn run build:dist",
|
||||||
|
"build:components:lib": "cd packages/components && yarn run build",
|
||||||
"build:ext-host": "cd packages/extension && yarn run build:ext-host",
|
"build:ext-host": "cd packages/extension && yarn run build:ext-host",
|
||||||
"build:watcher-host": "cd packages/file-service && yarn run build:watcher-host",
|
"build:watcher-host": "cd packages/file-service && yarn run build:watcher-host",
|
||||||
"build:monaco-worker": "cd packages/monaco && yarn run build:worker",
|
"build:monaco-worker": "cd packages/monaco && yarn run build:worker",
|
||||||
|
|
|
@ -25,7 +25,7 @@ describe('ChatAgentService', () => {
|
||||||
{
|
{
|
||||||
token: ChatAgentPromptProvider,
|
token: ChatAgentPromptProvider,
|
||||||
useValue: {
|
useValue: {
|
||||||
provideContextPrompt: (val, msg) => msg,
|
provideContextPrompt: async (val, msg) => msg,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -59,7 +59,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
|
||||||
private readonly aiReporter: IAIReporter;
|
private readonly aiReporter: IAIReporter;
|
||||||
|
|
||||||
@Autowired(LLMContextServiceToken)
|
@Autowired(LLMContextServiceToken)
|
||||||
protected readonly contextService: LLMContextService;
|
protected readonly llmContextService: LLMContextService;
|
||||||
|
|
||||||
@Autowired(ChatAgentPromptProvider)
|
@Autowired(ChatAgentPromptProvider)
|
||||||
protected readonly promptProvider: ChatAgentPromptProvider;
|
protected readonly promptProvider: ChatAgentPromptProvider;
|
||||||
|
@ -74,7 +74,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
|
||||||
super();
|
super();
|
||||||
this.addDispose(this._onDidChangeAgents);
|
this.addDispose(this._onDidChangeAgents);
|
||||||
this.addDispose(
|
this.addDispose(
|
||||||
this.contextService.onDidContextFilesChangeEvent((event) => {
|
this.llmContextService.onDidContextFilesChangeEvent((event) => {
|
||||||
if (event.version !== this.contextVersion) {
|
if (event.version !== this.contextVersion) {
|
||||||
this.contextVersion = event.version;
|
this.contextVersion = event.version;
|
||||||
this.shouldUpdateContext = true;
|
this.shouldUpdateContext = true;
|
||||||
|
@ -152,9 +152,9 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
|
||||||
if (!this.initialUserMessageMap.has(request.sessionId)) {
|
if (!this.initialUserMessageMap.has(request.sessionId)) {
|
||||||
this.initialUserMessageMap.set(request.sessionId, request.message);
|
this.initialUserMessageMap.set(request.sessionId, request.message);
|
||||||
const rawMessage = request.message;
|
const rawMessage = request.message;
|
||||||
request.message = this.provideContextMessage(rawMessage, request.sessionId);
|
request.message = await this.provideContextMessage(rawMessage, request.sessionId);
|
||||||
} else if (this.shouldUpdateContext || request.regenerate || history.length === 0) {
|
} else if (this.shouldUpdateContext || request.regenerate || history.length === 0) {
|
||||||
request.message = this.provideContextMessage(request.message, request.sessionId);
|
request.message = await this.provideContextMessage(request.message, request.sessionId);
|
||||||
this.shouldUpdateContext = false;
|
this.shouldUpdateContext = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,9 +162,9 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private provideContextMessage(message: string, sessionId: string) {
|
private async provideContextMessage(message: string, sessionId: string) {
|
||||||
const context = this.contextService.serialize();
|
const context = await this.llmContextService.serialize();
|
||||||
const fullMessage = this.promptProvider.provideContextPrompt(context, message);
|
const fullMessage = await this.promptProvider.provideContextPrompt(context, message);
|
||||||
this.aiReporter.send({
|
this.aiReporter.send({
|
||||||
msgType: AIServiceType.Chat,
|
msgType: AIServiceType.Chat,
|
||||||
actionType: ActionTypeEnum.ContextEnhance,
|
actionType: ActionTypeEnum.ContextEnhance,
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { MessageList } from 'react-chat-elements';
|
import { MessageList } from 'react-chat-elements';
|
||||||
|
|
||||||
import { AppConfig, getIcon, useInjectable, useUpdateOnEvent } from '@opensumi/ide-core-browser';
|
import {
|
||||||
|
AINativeConfigService,
|
||||||
|
AppConfig,
|
||||||
|
LabelService,
|
||||||
|
getIcon,
|
||||||
|
useInjectable,
|
||||||
|
useUpdateOnEvent,
|
||||||
|
} from '@opensumi/ide-core-browser';
|
||||||
import { Popover, PopoverPosition } from '@opensumi/ide-core-browser/lib/components';
|
import { Popover, PopoverPosition } from '@opensumi/ide-core-browser/lib/components';
|
||||||
import { EnhanceIcon } from '@opensumi/ide-core-browser/lib/components/ai-native';
|
import { EnhanceIcon } from '@opensumi/ide-core-browser/lib/components/ai-native';
|
||||||
import {
|
import {
|
||||||
|
@ -14,6 +21,7 @@ import {
|
||||||
ChatMessageRole,
|
ChatMessageRole,
|
||||||
ChatRenderRegistryToken,
|
ChatRenderRegistryToken,
|
||||||
ChatServiceToken,
|
ChatServiceToken,
|
||||||
|
CommandService,
|
||||||
Disposable,
|
Disposable,
|
||||||
DisposableCollection,
|
DisposableCollection,
|
||||||
IAIReporter,
|
IAIReporter,
|
||||||
|
@ -28,16 +36,19 @@ import {
|
||||||
import { WorkbenchEditorService } from '@opensumi/ide-editor';
|
import { WorkbenchEditorService } from '@opensumi/ide-editor';
|
||||||
import { IMainLayoutService } from '@opensumi/ide-main-layout';
|
import { IMainLayoutService } from '@opensumi/ide-main-layout';
|
||||||
import { IMessageService } from '@opensumi/ide-overlay';
|
import { IMessageService } from '@opensumi/ide-overlay';
|
||||||
|
|
||||||
import 'react-chat-elements/dist/main.css';
|
import 'react-chat-elements/dist/main.css';
|
||||||
|
import { IWorkspaceService } from '@opensumi/ide-workspace';
|
||||||
|
|
||||||
import { AI_CHAT_VIEW_ID, IChatAgentService, IChatInternalService, IChatMessageStructure } from '../../common';
|
import { AI_CHAT_VIEW_ID, IChatAgentService, IChatInternalService, IChatMessageStructure } from '../../common';
|
||||||
|
import { LLMContextService, LLMContextServiceToken } from '../../common/llm-context';
|
||||||
import { CodeBlockData } from '../../common/types';
|
import { CodeBlockData } from '../../common/types';
|
||||||
|
import { cleanAttachedTextWrapper } from '../../common/utils';
|
||||||
import { FileChange, FileListDisplay } from '../components/ChangeList';
|
import { FileChange, FileListDisplay } from '../components/ChangeList';
|
||||||
import { ChatContext } from '../components/ChatContext';
|
|
||||||
import { CodeBlockWrapperInput } from '../components/ChatEditor';
|
import { CodeBlockWrapperInput } from '../components/ChatEditor';
|
||||||
import ChatHistory, { IChatHistoryItem } from '../components/ChatHistory';
|
import ChatHistory, { IChatHistoryItem } from '../components/ChatHistory';
|
||||||
import { ChatInput } from '../components/ChatInput';
|
import { ChatInput } from '../components/ChatInput';
|
||||||
import { ChatMarkdown } from '../components/ChatMarkdown';
|
import { ChatMarkdown } from '../components/ChatMarkdown';
|
||||||
|
import { ChatMentionInput } from '../components/ChatMentionInput';
|
||||||
import { ChatNotify, ChatReply } from '../components/ChatReply';
|
import { ChatNotify, ChatReply } from '../components/ChatReply';
|
||||||
import { SlashCustomRender } from '../components/SlashCustomRender';
|
import { SlashCustomRender } from '../components/SlashCustomRender';
|
||||||
import { MessageData, createMessageByAI, createMessageByUser } from '../components/utils';
|
import { MessageData, createMessageByAI, createMessageByUser } from '../components/utils';
|
||||||
|
@ -105,6 +116,8 @@ export const AIChatView = () => {
|
||||||
const chatFeatureRegistry = useInjectable<ChatFeatureRegistry>(ChatFeatureRegistryToken);
|
const chatFeatureRegistry = useInjectable<ChatFeatureRegistry>(ChatFeatureRegistryToken);
|
||||||
const chatRenderRegistry = useInjectable<ChatRenderRegistry>(ChatRenderRegistryToken);
|
const chatRenderRegistry = useInjectable<ChatRenderRegistry>(ChatRenderRegistryToken);
|
||||||
const mcpServerRegistry = useInjectable<IMCPServerRegistry>(TokenMCPServerRegistry);
|
const mcpServerRegistry = useInjectable<IMCPServerRegistry>(TokenMCPServerRegistry);
|
||||||
|
const aiNativeConfigService = useInjectable<AINativeConfigService>(AINativeConfigService);
|
||||||
|
const llmContextService = useInjectable<LLMContextService>(LLMContextServiceToken);
|
||||||
|
|
||||||
const layoutService = useInjectable<IMainLayoutService>(IMainLayoutService);
|
const layoutService = useInjectable<IMainLayoutService>(IMainLayoutService);
|
||||||
const msgHistoryManager = aiChatService.sessionModel.history;
|
const msgHistoryManager = aiChatService.sessionModel.history;
|
||||||
|
@ -114,6 +127,9 @@ export const AIChatView = () => {
|
||||||
const editorService = useInjectable<WorkbenchEditorService>(WorkbenchEditorService);
|
const editorService = useInjectable<WorkbenchEditorService>(WorkbenchEditorService);
|
||||||
const appConfig = useInjectable<AppConfig>(AppConfig);
|
const appConfig = useInjectable<AppConfig>(AppConfig);
|
||||||
const applyService = useInjectable<BaseApplyService>(BaseApplyService);
|
const applyService = useInjectable<BaseApplyService>(BaseApplyService);
|
||||||
|
const labelService = useInjectable<LabelService>(LabelService);
|
||||||
|
const workspaceService = useInjectable<IWorkspaceService>(IWorkspaceService);
|
||||||
|
const commandService = useInjectable<CommandService>(CommandService);
|
||||||
const [shortcutCommands, setShortcutCommands] = React.useState<ChatSlashCommandItemModel[]>([]);
|
const [shortcutCommands, setShortcutCommands] = React.useState<ChatSlashCommandItemModel[]>([]);
|
||||||
|
|
||||||
const [changeList, setChangeList] = React.useState<FileChange[]>(getFileChanges(applyService.getSessionCodeBlocks()));
|
const [changeList, setChangeList] = React.useState<FileChange[]>(getFileChanges(applyService.getSessionCodeBlocks()));
|
||||||
|
@ -184,6 +200,9 @@ export const AIChatView = () => {
|
||||||
if (chatRenderRegistry.chatInputRender) {
|
if (chatRenderRegistry.chatInputRender) {
|
||||||
return chatRenderRegistry.chatInputRender;
|
return chatRenderRegistry.chatInputRender;
|
||||||
}
|
}
|
||||||
|
if (aiNativeConfigService.capabilities.supportsMCP) {
|
||||||
|
return ChatMentionInput;
|
||||||
|
}
|
||||||
return ChatInput;
|
return ChatInput;
|
||||||
}, [chatRenderRegistry.chatInputRender]);
|
}, [chatRenderRegistry.chatInputRender]);
|
||||||
|
|
||||||
|
@ -262,7 +281,7 @@ export const AIChatView = () => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await handleSend(message);
|
await handleSend(message.message, message.agentId, message.command);
|
||||||
} else {
|
} else {
|
||||||
if (message.agentId) {
|
if (message.agentId) {
|
||||||
setAgentId(message.agentId);
|
setAgentId(message.agentId);
|
||||||
|
@ -349,6 +368,9 @@ export const AIChatView = () => {
|
||||||
text={message}
|
text={message}
|
||||||
agentId={visibleAgentId}
|
agentId={visibleAgentId}
|
||||||
command={command}
|
command={command}
|
||||||
|
labelService={labelService}
|
||||||
|
workspaceService={workspaceService}
|
||||||
|
commandService={commandService}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -454,7 +476,15 @@ export const AIChatView = () => {
|
||||||
text: ChatUserRoleRender ? (
|
text: ChatUserRoleRender ? (
|
||||||
<ChatUserRoleRender content={message} agentId={visibleAgentId} command={command} />
|
<ChatUserRoleRender content={message} agentId={visibleAgentId} command={command} />
|
||||||
) : (
|
) : (
|
||||||
<CodeBlockWrapperInput relationId={relationId} text={message} agentId={visibleAgentId} command={command} />
|
<CodeBlockWrapperInput
|
||||||
|
labelService={labelService}
|
||||||
|
relationId={relationId}
|
||||||
|
text={message}
|
||||||
|
agentId={visibleAgentId}
|
||||||
|
command={command}
|
||||||
|
workspaceService={workspaceService}
|
||||||
|
commandService={commandService}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
styles.chat_message_code,
|
styles.chat_message_code,
|
||||||
|
@ -634,15 +664,50 @@ export const AIChatView = () => {
|
||||||
msgId,
|
msgId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[chatRenderRegistry, chatRenderRegistry.chatUserRoleRender, msgHistoryManager, scrollToBottom],
|
[chatRenderRegistry, chatRenderRegistry.chatUserRoleRender, msgHistoryManager, scrollToBottom, loading],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSend = React.useCallback(
|
const handleSend = React.useCallback(
|
||||||
async (value: IChatMessageStructure) => {
|
async (message: string, agentId?: string, command?: string) => {
|
||||||
const { message, command, reportExtra } = value;
|
const reportExtra = {
|
||||||
|
actionSource: ActionSourceEnum.Chat,
|
||||||
|
actionType: ActionTypeEnum.Send,
|
||||||
|
};
|
||||||
|
agentId = agentId ? agentId : ChatProxyService.AGENT_ID;
|
||||||
|
// 提取并替换 {{@file:xxx}} 中的文件内容
|
||||||
|
let processedContent = message;
|
||||||
|
const filePattern = /\{\{@file:(.*?)\}\}/g;
|
||||||
|
const fileMatches = message.match(filePattern);
|
||||||
|
let isCleanContext = false;
|
||||||
|
if (fileMatches) {
|
||||||
|
for (const match of fileMatches) {
|
||||||
|
const filePath = match.replace(/\{\{@file:(.*?)\}\}/, '$1');
|
||||||
|
if (filePath && !isCleanContext) {
|
||||||
|
isCleanContext = true;
|
||||||
|
llmContextService.cleanFileContext();
|
||||||
|
}
|
||||||
|
const fileUri = new URI(filePath);
|
||||||
|
llmContextService.addFileToContext(fileUri, undefined, true);
|
||||||
|
const relativePath = (await workspaceService.asRelativePath(fileUri))?.path || fileUri.displayName;
|
||||||
|
// 获取文件内容
|
||||||
|
// 替换占位符,后续支持自定义渲染时可替换为自定义渲染标签
|
||||||
|
processedContent = processedContent.replace(match, `\`<attached_file>${relativePath}\``);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const agentId = value.agentId ? value.agentId : ChatProxyService.AGENT_ID;
|
const folderPattern = /\{\{@folder:(.*?)\}\}/g;
|
||||||
return handleAgentReply({ message, agentId, command, reportExtra });
|
const folderMatches = processedContent.match(folderPattern);
|
||||||
|
if (folderMatches) {
|
||||||
|
for (const match of folderMatches) {
|
||||||
|
const folderPath = match.replace(/\{\{@folder:(.*?)\}\}/, '$1');
|
||||||
|
const folderUri = new URI(folderPath);
|
||||||
|
llmContextService.addFolderToContext(folderUri);
|
||||||
|
const relativePath = (await workspaceService.asRelativePath(folderUri))?.path || folderUri.displayName;
|
||||||
|
// 替换占位符,后续支持自定义渲染时可替换为自定义渲染标签
|
||||||
|
processedContent = processedContent.replace(match, `\`<attached_folder>${relativePath}\``);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return handleAgentReply({ message: processedContent, agentId, command, reportExtra });
|
||||||
},
|
},
|
||||||
[handleAgentReply],
|
[handleAgentReply],
|
||||||
);
|
);
|
||||||
|
@ -759,7 +824,6 @@ export const AIChatView = () => {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className={styles.chat_input_wrap}>
|
<div className={styles.chat_input_wrap}>
|
||||||
<ChatContext />
|
|
||||||
<div className={styles.header_operate}>
|
<div className={styles.header_operate}>
|
||||||
<div className={styles.header_operate_left}>
|
<div className={styles.header_operate_left}>
|
||||||
{shortcutCommands.map((command) => (
|
{shortcutCommands.map((command) => (
|
||||||
|
@ -790,17 +854,7 @@ export const AIChatView = () => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ChatInputWrapperRender
|
<ChatInputWrapperRender
|
||||||
onSend={(value, agentId, command) =>
|
onSend={handleSend}
|
||||||
handleSend({
|
|
||||||
message: value,
|
|
||||||
agentId,
|
|
||||||
command,
|
|
||||||
reportExtra: {
|
|
||||||
actionSource: ActionSourceEnum.Chat,
|
|
||||||
actionType: ActionTypeEnum.Send,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
enableOptions={true}
|
enableOptions={true}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
@ -857,12 +911,15 @@ export function DefaultChatViewHeader({
|
||||||
const getHistoryList = () => {
|
const getHistoryList = () => {
|
||||||
const currentMessages = aiChatService.sessionModel.history.getMessages();
|
const currentMessages = aiChatService.sessionModel.history.getMessages();
|
||||||
const latestUserMessage = currentMessages.findLast((m) => m.role === ChatMessageRole.User);
|
const latestUserMessage = currentMessages.findLast((m) => m.role === ChatMessageRole.User);
|
||||||
setCurrentTitle(latestUserMessage ? latestUserMessage.content.slice(0, MAX_TITLE_LENGTH) : '');
|
setCurrentTitle(
|
||||||
|
latestUserMessage ? cleanAttachedTextWrapper(latestUserMessage.content).slice(0, MAX_TITLE_LENGTH) : '',
|
||||||
|
);
|
||||||
setHistoryList(
|
setHistoryList(
|
||||||
aiChatService.getSessions().map((session) => {
|
aiChatService.getSessions().map((session) => {
|
||||||
const history = session.history;
|
const history = session.history;
|
||||||
const messages = history.getMessages();
|
const messages = history.getMessages();
|
||||||
const title = messages.length > 0 ? messages[0].content.slice(0, MAX_TITLE_LENGTH) : '';
|
const title =
|
||||||
|
messages.length > 0 ? cleanAttachedTextWrapper(messages[0].content).slice(0, MAX_TITLE_LENGTH) : '';
|
||||||
const updatedAt = messages.length > 0 ? messages[messages.length - 1].replyStartTime || 0 : 0;
|
const updatedAt = messages.length > 0 ? messages[messages.length - 1].replyStartTime || 0 : 0;
|
||||||
// const loading = session.requests[session.requests.length - 1]?.response.isComplete;
|
// const loading = session.requests[session.requests.length - 1]?.response.isComplete;
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -2,14 +2,24 @@ import capitalize from 'lodash/capitalize';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import Highlight from 'react-highlight';
|
import Highlight from 'react-highlight';
|
||||||
|
|
||||||
import { IClipboardService, getIcon, useInjectable, uuid } from '@opensumi/ide-core-browser';
|
import {
|
||||||
import { Popover } from '@opensumi/ide-core-browser/lib/components';
|
EDITOR_COMMANDS,
|
||||||
|
FILE_COMMANDS,
|
||||||
|
IClipboardService,
|
||||||
|
LabelService,
|
||||||
|
getIcon,
|
||||||
|
useInjectable,
|
||||||
|
uuid,
|
||||||
|
} from '@opensumi/ide-core-browser';
|
||||||
|
import { Icon, Popover } from '@opensumi/ide-core-browser/lib/components';
|
||||||
import { EnhanceIcon } from '@opensumi/ide-core-browser/lib/components/ai-native';
|
import { EnhanceIcon } from '@opensumi/ide-core-browser/lib/components/ai-native';
|
||||||
import {
|
import {
|
||||||
ActionSourceEnum,
|
ActionSourceEnum,
|
||||||
ActionTypeEnum,
|
ActionTypeEnum,
|
||||||
ChatFeatureRegistryToken,
|
ChatFeatureRegistryToken,
|
||||||
|
CommandService,
|
||||||
IAIReporter,
|
IAIReporter,
|
||||||
|
URI,
|
||||||
localize,
|
localize,
|
||||||
runWhenIdle,
|
runWhenIdle,
|
||||||
} from '@opensumi/ide-core-common';
|
} from '@opensumi/ide-core-common';
|
||||||
|
@ -22,6 +32,9 @@ import { ChatFeatureRegistry } from '../chat/chat.feature.registry';
|
||||||
|
|
||||||
import styles from './components.module.less';
|
import styles from './components.module.less';
|
||||||
import { highLightLanguageSupport } from './highLight';
|
import { highLightLanguageSupport } from './highLight';
|
||||||
|
import { MentionType } from './mention-input/types';
|
||||||
|
|
||||||
|
import type { IWorkspaceService } from '@opensumi/ide-workspace';
|
||||||
|
|
||||||
import './highlightTheme.less';
|
import './highlightTheme.less';
|
||||||
|
|
||||||
|
@ -139,16 +152,56 @@ const CodeBlock = ({
|
||||||
renderText,
|
renderText,
|
||||||
agentId = '',
|
agentId = '',
|
||||||
command = '',
|
command = '',
|
||||||
|
labelService,
|
||||||
|
commandService,
|
||||||
|
workspaceService,
|
||||||
}: {
|
}: {
|
||||||
content?: string;
|
content?: string;
|
||||||
relationId: string;
|
relationId: string;
|
||||||
renderText?: (t: string) => React.ReactNode;
|
renderText?: (t: string) => React.ReactNode;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
command?: string;
|
command?: string;
|
||||||
|
labelService?: LabelService;
|
||||||
|
commandService?: CommandService;
|
||||||
|
workspaceService?: IWorkspaceService;
|
||||||
}) => {
|
}) => {
|
||||||
const rgInlineCode = /`([^`]+)`/g;
|
const rgInlineCode = /`([^`]+)`/g;
|
||||||
const rgBlockCode = /```([^]+?)```/g;
|
const rgBlockCode = /```([^]+?)```/g;
|
||||||
const rgBlockCodeBefore = /```([^]+)?/g;
|
const rgBlockCodeBefore = /```([^]+)?/g;
|
||||||
|
const rgAttachedFile = /<attached_file>(.*)/g;
|
||||||
|
const rgAttachedFolder = /<attached_folder>(.*)/g;
|
||||||
|
const handleAttachmentClick = useCallback(
|
||||||
|
async (text: string, type: MentionType) => {
|
||||||
|
const roots = await workspaceService?.roots;
|
||||||
|
let uri;
|
||||||
|
if (!roots) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const root of roots) {
|
||||||
|
uri = new URI(root.uri).resolve(text);
|
||||||
|
try {
|
||||||
|
await commandService?.executeCommand(FILE_COMMANDS.REVEAL_IN_EXPLORER.id, uri);
|
||||||
|
if (type === MentionType.FILE) {
|
||||||
|
await commandService?.executeCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, uri);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[commandService, workspaceService],
|
||||||
|
);
|
||||||
|
const renderAttachment = (text: string, isFolder = false, key: string) => (
|
||||||
|
<span
|
||||||
|
className={styles.attachment}
|
||||||
|
key={key}
|
||||||
|
onClick={() => handleAttachmentClick(text, isFolder ? MentionType.FOLDER : MentionType.FILE)}
|
||||||
|
>
|
||||||
|
<Icon iconClass={isFolder ? getIcon('folder') : labelService?.getIcon(new URI(text || 'file'))} />
|
||||||
|
<span className={styles.attachment_text}>{text}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
const renderCodeEditor = (content: string) => {
|
const renderCodeEditor = (content: string) => {
|
||||||
const language = content.split('\n')[0].trim().toLowerCase();
|
const language = content.split('\n')[0].trim().toLowerCase();
|
||||||
|
@ -192,6 +245,43 @@ const CodeBlock = ({
|
||||||
} else {
|
} else {
|
||||||
renderedContent.push(text);
|
renderedContent.push(text);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 处理文件和文件夹标记
|
||||||
|
const processedText = text;
|
||||||
|
const fileMatches = [...text.matchAll(rgAttachedFile)];
|
||||||
|
const folderMatches = [...text.matchAll(rgAttachedFolder)];
|
||||||
|
if (fileMatches.length || folderMatches.length) {
|
||||||
|
let lastIndex = 0;
|
||||||
|
const fragments: (string | React.ReactNode)[] = [];
|
||||||
|
|
||||||
|
// 通用处理函数
|
||||||
|
const processMatches = (matches: RegExpMatchArray[], isFolder: boolean) => {
|
||||||
|
matches.forEach((match, matchIndex) => {
|
||||||
|
if (match.index !== undefined) {
|
||||||
|
const spanText = processedText.slice(lastIndex, match.index);
|
||||||
|
if (spanText) {
|
||||||
|
fragments.push(
|
||||||
|
<span key={`${index}-${matchIndex}-${isFolder ? 'folder' : 'file'}`}>{spanText}</span>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
fragments.push(
|
||||||
|
renderAttachment(
|
||||||
|
match[1],
|
||||||
|
isFolder,
|
||||||
|
`${index}-tag-${matchIndex}-${isFolder ? 'folder' : 'file'}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
lastIndex = match.index + match[0].length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理文件标记
|
||||||
|
processMatches(fileMatches, false);
|
||||||
|
processMatches(folderMatches, true);
|
||||||
|
|
||||||
|
fragments.push(processedText.slice(lastIndex));
|
||||||
|
renderedContent.push(...fragments);
|
||||||
} else {
|
} else {
|
||||||
renderedContent.push(
|
renderedContent.push(
|
||||||
<span className={styles.code_inline} key={index}>
|
<span className={styles.code_inline} key={index}>
|
||||||
|
@ -199,6 +289,7 @@ const CodeBlock = ({
|
||||||
</span>,
|
</span>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
renderedContent.push(renderCodeEditor(block));
|
renderedContent.push(renderCodeEditor(block));
|
||||||
|
@ -216,15 +307,29 @@ export const CodeBlockWrapper = ({
|
||||||
renderText,
|
renderText,
|
||||||
relationId,
|
relationId,
|
||||||
agentId,
|
agentId,
|
||||||
|
labelService,
|
||||||
|
commandService,
|
||||||
|
workspaceService,
|
||||||
}: {
|
}: {
|
||||||
text?: string;
|
text?: string;
|
||||||
relationId: string;
|
relationId: string;
|
||||||
renderText?: (t: string) => React.ReactNode;
|
renderText?: (t: string) => React.ReactNode;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
|
labelService?: LabelService;
|
||||||
|
commandService?: CommandService;
|
||||||
|
workspaceService?: IWorkspaceService;
|
||||||
}) => (
|
}) => (
|
||||||
<div className={styles.ai_chat_code_wrapper}>
|
<div className={styles.ai_chat_code_wrapper}>
|
||||||
<div className={styles.render_text}>
|
<div className={styles.render_text}>
|
||||||
<CodeBlock content={text} renderText={renderText} relationId={relationId} agentId={agentId} />
|
<CodeBlock
|
||||||
|
content={text}
|
||||||
|
labelService={labelService}
|
||||||
|
renderText={renderText}
|
||||||
|
relationId={relationId}
|
||||||
|
agentId={agentId}
|
||||||
|
commandService={commandService}
|
||||||
|
workspaceService={workspaceService}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -234,11 +339,17 @@ export const CodeBlockWrapperInput = ({
|
||||||
relationId,
|
relationId,
|
||||||
agentId,
|
agentId,
|
||||||
command,
|
command,
|
||||||
|
labelService,
|
||||||
|
workspaceService,
|
||||||
|
commandService,
|
||||||
}: {
|
}: {
|
||||||
text: string;
|
text: string;
|
||||||
relationId: string;
|
relationId: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
command?: string;
|
command?: string;
|
||||||
|
labelService?: LabelService;
|
||||||
|
workspaceService?: IWorkspaceService;
|
||||||
|
commandService?: CommandService;
|
||||||
}) => {
|
}) => {
|
||||||
const chatFeatureRegistry = useInjectable<ChatFeatureRegistry>(ChatFeatureRegistryToken);
|
const chatFeatureRegistry = useInjectable<ChatFeatureRegistry>(ChatFeatureRegistryToken);
|
||||||
const [tag, setTag] = useState<string>('');
|
const [tag, setTag] = useState<string>('');
|
||||||
|
@ -271,7 +382,15 @@ export const CodeBlockWrapperInput = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{command && <div className={styles.tag}>/ {command}</div>}
|
{command && <div className={styles.tag}>/ {command}</div>}
|
||||||
<CodeBlock content={txt} relationId={relationId} agentId={agentId} command={command} />
|
<CodeBlock
|
||||||
|
content={txt}
|
||||||
|
labelService={labelService}
|
||||||
|
relationId={relationId}
|
||||||
|
agentId={agentId}
|
||||||
|
command={command}
|
||||||
|
workspaceService={workspaceService}
|
||||||
|
commandService={commandService}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -163,25 +163,22 @@ const ChatHistory: FC<IChatHistoryProps> = memo(
|
||||||
(item: IChatHistoryItem) => (
|
(item: IChatHistoryItem) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className={cls(
|
className={cls(styles.chat_history_item, item.id === currentId ? styles.chat_history_item_selected : '')}
|
||||||
styles['dm-chat-history-item'],
|
|
||||||
item.id === currentId ? styles['dm-chat-history-item-selected'] : '',
|
|
||||||
)}
|
|
||||||
onClick={() => handleHistoryItemSelect(item)}
|
onClick={() => handleHistoryItemSelect(item)}
|
||||||
>
|
>
|
||||||
<div className={styles['dm-chat-history-item-content']}>
|
<div className={styles.chat_history_item_content}>
|
||||||
{item.loading ? (
|
{item.loading ? (
|
||||||
<Loading />
|
<Loading />
|
||||||
) : (
|
) : (
|
||||||
<Icon icon='message' style={{ width: '16px', height: '16px', marginRight: 4 }} />
|
<Icon icon='message' style={{ width: '16px', height: '16px', marginRight: 4 }} />
|
||||||
)}
|
)}
|
||||||
{!historyTitleEditable?.[item.id] ? (
|
{!historyTitleEditable?.[item.id] ? (
|
||||||
<span id={`dm-chat-history-item-title-${item.id}`} className={styles['dm-chat-history-item-title']}>
|
<span id={`chat-history-item-title-${item.id}`} className={styles.chat_history_item_title}>
|
||||||
{item.title}
|
{item.title}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
className={styles['dm-chat-history-item-title']}
|
className={styles.chat_history_item_title}
|
||||||
defaultValue={item.title}
|
defaultValue={item.title}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
onPressEnter={(e: any) => {
|
onPressEnter={(e: any) => {
|
||||||
|
@ -191,18 +188,9 @@ const ChatHistory: FC<IChatHistoryProps> = memo(
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['dm-chat-history-item-actions']}>
|
<div className={styles.chat_history_item_actions}>
|
||||||
{/* <EditOutlined
|
|
||||||
title={localize('aiNative.operate.chatHistory.edit')}
|
|
||||||
style={{ marginRight: 8 }}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleTitleEdit(item);
|
|
||||||
}}
|
|
||||||
/> */}
|
|
||||||
<EnhanceIcon
|
<EnhanceIcon
|
||||||
className={cls(styles['dm-chat-history-item-actions-delete'], getIcon('delete'))}
|
className={cls(styles.chat_history_item_actions_delete, getIcon('delete'))}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -237,14 +225,14 @@ const ChatHistory: FC<IChatHistoryProps> = memo(
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
placeholder={localize('aiNative.operate.chatHistory.searchPlaceholder')}
|
placeholder={localize('aiNative.operate.chatHistory.searchPlaceholder')}
|
||||||
style={{ width: '100%', maxWidth: '100%' }}
|
className={styles.chat_history_search}
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
/>
|
/>
|
||||||
<div className={styles['dm-chat-history-list']}>
|
<div className={styles.chat_history_list}>
|
||||||
{groupedHistoryList.map((group) => (
|
{groupedHistoryList.map((group) => (
|
||||||
<div key={group.key} style={{ padding: '4px' }}>
|
<div key={group.key} style={{ padding: '4px' }}>
|
||||||
<div className={styles['dm-chat-history-time']}>{group.key}</div>
|
<div className={styles.chat_history_time}>{group.key}</div>
|
||||||
{group.items.map(renderHistoryItem)}
|
{group.items.map(renderHistoryItem)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
@ -257,13 +245,13 @@ const ChatHistory: FC<IChatHistoryProps> = memo(
|
||||||
const getPopupContainer = useCallback((triggerNode: HTMLElement) => triggerNode.parentElement!, []);
|
const getPopupContainer = useCallback((triggerNode: HTMLElement) => triggerNode.parentElement!, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cls(styles['dm-chat-history-header'], className)}>
|
<div className={cls(styles.chat_history_header, className)}>
|
||||||
<div className={styles['dm-chat-history-header-title']}>
|
<div className={styles.chat_history_header_title}>
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['dm-chat-history-header-actions']}>
|
<div className={styles.chat_history_header_actions}>
|
||||||
<Popover
|
<Popover
|
||||||
id='dm-chat-history-header-actions-history'
|
id='chat-history-header-actions-history'
|
||||||
content={renderHistory()}
|
content={renderHistory()}
|
||||||
trigger={PopoverTriggerType.click}
|
trigger={PopoverTriggerType.click}
|
||||||
position={PopoverPosition.bottomRight}
|
position={PopoverPosition.bottomRight}
|
||||||
|
@ -271,12 +259,10 @@ const ChatHistory: FC<IChatHistoryProps> = memo(
|
||||||
getPopupContainer={getPopupContainer}
|
getPopupContainer={getPopupContainer}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={styles['dm-chat-history-header-actions-history']}
|
className={styles.chat_history_header_actions_history}
|
||||||
title={localize('aiNative.operate.chatHistory.title')}
|
title={localize('aiNative.operate.chatHistory.title')}
|
||||||
>
|
>
|
||||||
<EnhanceIcon
|
<EnhanceIcon className={cls(styles.chat_history_header_actions_history, 'codicon codicon-history')} />
|
||||||
className={cls(styles['dm-chat-history-header-actions-history'], 'codicon codicon-history')}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Popover
|
<Popover
|
||||||
|
@ -285,7 +271,7 @@ const ChatHistory: FC<IChatHistoryProps> = memo(
|
||||||
title={localize('aiNative.operate.newChat.title')}
|
title={localize('aiNative.operate.newChat.title')}
|
||||||
>
|
>
|
||||||
<EnhanceIcon
|
<EnhanceIcon
|
||||||
className={cls(styles['dm-chat-history-header-actions-new'], getIcon('plus'))}
|
className={cls(styles.chat_history_header_actions_new, getIcon('plus'))}
|
||||||
onClick={handleNewChat}
|
onClick={handleNewChat}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
|
@ -0,0 +1,276 @@
|
||||||
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { LabelService, RecentFilesManager, useInjectable } from '@opensumi/ide-core-browser';
|
||||||
|
import { getIcon } from '@opensumi/ide-core-browser/lib/components';
|
||||||
|
import { URI, localize } from '@opensumi/ide-core-common';
|
||||||
|
import { CommandService } from '@opensumi/ide-core-common/lib/command';
|
||||||
|
import { WorkbenchEditorService } from '@opensumi/ide-editor';
|
||||||
|
import { FileSearchServicePath, IFileSearchService } from '@opensumi/ide-file-search';
|
||||||
|
import { IWorkspaceService } from '@opensumi/ide-workspace';
|
||||||
|
|
||||||
|
import { IChatInternalService } from '../../common';
|
||||||
|
import { ChatInternalService } from '../chat/chat.internal.service';
|
||||||
|
import { OPEN_MCP_CONFIG_COMMAND } from '../mcp/config/mcp-config.commands';
|
||||||
|
|
||||||
|
import styles from './components.module.less';
|
||||||
|
import { MentionInput } from './mention-input/mention-input';
|
||||||
|
import { FooterButtonPosition, FooterConfig, MentionItem, MentionType } from './mention-input/types';
|
||||||
|
|
||||||
|
export interface IChatMentionInputProps {
|
||||||
|
onSend: (value: string, agentId?: string, command?: string, option?: { model: string; [key: string]: any }) => void;
|
||||||
|
onValueChange?: (value: string) => void;
|
||||||
|
onExpand?: (value: boolean) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
enableOptions?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
sendBtnClassName?: string;
|
||||||
|
defaultHeight?: number;
|
||||||
|
value?: string;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
theme?: string | null;
|
||||||
|
setTheme: (theme: string | null) => void;
|
||||||
|
agentId: string;
|
||||||
|
setAgentId: (id: string) => void;
|
||||||
|
defaultAgentId?: string;
|
||||||
|
command: string;
|
||||||
|
setCommand: (command: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 指令命令激活组件
|
||||||
|
export const ChatMentionInput = (props: IChatMentionInputProps) => {
|
||||||
|
const { onSend, disabled = false } = props;
|
||||||
|
|
||||||
|
const [value, setValue] = useState(props.value || '');
|
||||||
|
const aiChatService = useInjectable<ChatInternalService>(IChatInternalService);
|
||||||
|
const commandService = useInjectable<CommandService>(CommandService);
|
||||||
|
const searchService = useInjectable<IFileSearchService>(FileSearchServicePath);
|
||||||
|
const recentFilesManager = useInjectable<RecentFilesManager>(RecentFilesManager);
|
||||||
|
const workspaceService = useInjectable<IWorkspaceService>(IWorkspaceService);
|
||||||
|
const editorService = useInjectable<WorkbenchEditorService>(WorkbenchEditorService);
|
||||||
|
const labelService = useInjectable<LabelService>(LabelService);
|
||||||
|
|
||||||
|
const handleShowMCPConfig = React.useCallback(() => {
|
||||||
|
commandService.executeCommand(OPEN_MCP_CONFIG_COMMAND.id);
|
||||||
|
}, [commandService]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.value !== value) {
|
||||||
|
setValue(props.value || '');
|
||||||
|
}
|
||||||
|
}, [props.value]);
|
||||||
|
|
||||||
|
// 默认菜单项
|
||||||
|
const defaultMenuItems: MentionItem[] = [
|
||||||
|
// {
|
||||||
|
// id: 'code',
|
||||||
|
// type: 'code',
|
||||||
|
// text: 'Code',
|
||||||
|
// icon: getIcon('codebraces'),
|
||||||
|
// getHighestLevelItems: () => [],
|
||||||
|
// getItems: async (searchText: string) => {
|
||||||
|
// const currentEditor = editorService.currentEditor;
|
||||||
|
// if (!currentEditor) {
|
||||||
|
// return [];
|
||||||
|
// }
|
||||||
|
// const currentDocumentModel = currentEditor.currentDocumentModel;
|
||||||
|
// if (!currentDocumentModel) {
|
||||||
|
// return [];
|
||||||
|
// }
|
||||||
|
// const symbols = await commandService.executeCommand('_executeFormatDocumentProvider', currentDocumentModel.uri.codeUri);
|
||||||
|
// return [];
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
id: MentionType.FILE,
|
||||||
|
type: MentionType.FILE,
|
||||||
|
text: 'File',
|
||||||
|
icon: getIcon('file'),
|
||||||
|
getHighestLevelItems: () => {
|
||||||
|
const currentEditor = editorService.currentEditor;
|
||||||
|
const currentUri = currentEditor?.currentUri;
|
||||||
|
if (!currentUri) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: currentUri.codeUri.fsPath,
|
||||||
|
type: MentionType.FILE,
|
||||||
|
text: currentUri.displayName,
|
||||||
|
value: currentUri.codeUri.fsPath,
|
||||||
|
description: `(${localize('aiNative.chat.defaultContextFile')})`,
|
||||||
|
contextId: currentUri.codeUri.fsPath,
|
||||||
|
icon: labelService.getIcon(currentUri),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
getItems: async (searchText: string) => {
|
||||||
|
if (!searchText) {
|
||||||
|
const recentFile = await recentFilesManager.getMostRecentlyOpenedFiles();
|
||||||
|
return Promise.all(
|
||||||
|
recentFile.map(async (file) => {
|
||||||
|
const uri = new URI(file);
|
||||||
|
const relatveParentPath = (await workspaceService.asRelativePath(uri.parent))?.path;
|
||||||
|
return {
|
||||||
|
id: uri.codeUri.fsPath,
|
||||||
|
type: MentionType.FILE,
|
||||||
|
text: uri.displayName,
|
||||||
|
value: uri.codeUri.fsPath,
|
||||||
|
description: relatveParentPath || '',
|
||||||
|
contextId: uri.codeUri.fsPath,
|
||||||
|
icon: labelService.getIcon(uri),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const rootUris = (await workspaceService.roots).map((root) => new URI(root.uri).codeUri.fsPath.toString());
|
||||||
|
const results = await searchService.find(searchText, {
|
||||||
|
rootUris,
|
||||||
|
useGitIgnore: true,
|
||||||
|
noIgnoreParent: true,
|
||||||
|
fuzzyMatch: true,
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
return Promise.all(
|
||||||
|
results.map(async (file) => {
|
||||||
|
const uri = new URI(file);
|
||||||
|
const relatveParentPath = (await workspaceService.asRelativePath(uri.parent))?.path;
|
||||||
|
return {
|
||||||
|
id: uri.codeUri.fsPath,
|
||||||
|
type: MentionType.FILE,
|
||||||
|
text: uri.displayName,
|
||||||
|
value: uri.codeUri.fsPath,
|
||||||
|
description: relatveParentPath || '',
|
||||||
|
contextId: uri.codeUri.fsPath,
|
||||||
|
icon: labelService.getIcon(uri),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MentionType.FOLDER,
|
||||||
|
type: MentionType.FOLDER,
|
||||||
|
text: 'Folder',
|
||||||
|
icon: getIcon('folder'),
|
||||||
|
getHighestLevelItems: () => {
|
||||||
|
const currentEditor = editorService.currentEditor;
|
||||||
|
const currentFolderUri = currentEditor?.currentUri?.parent;
|
||||||
|
if (!currentFolderUri) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (currentFolderUri.toString() === workspaceService.workspace?.uri) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: currentFolderUri.codeUri.fsPath,
|
||||||
|
type: MentionType.FOLDER,
|
||||||
|
text: currentFolderUri.displayName,
|
||||||
|
value: currentFolderUri.codeUri.fsPath,
|
||||||
|
description: `(${localize('aiNative.chat.defaultContextFolder')})`,
|
||||||
|
contextId: currentFolderUri.codeUri.fsPath,
|
||||||
|
icon: getIcon('folder'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
getItems: async (searchText: string) => {
|
||||||
|
let folders: MentionItem[] = [];
|
||||||
|
if (!searchText) {
|
||||||
|
const recentFile = await recentFilesManager.getMostRecentlyOpenedFiles();
|
||||||
|
const recentFolder = Array.from(new Set(recentFile.map((file) => new URI(file).parent.codeUri.fsPath)));
|
||||||
|
folders = await Promise.all(
|
||||||
|
recentFolder.map(async (folder) => {
|
||||||
|
const uri = new URI(folder);
|
||||||
|
const relativePath = await workspaceService.asRelativePath(uri);
|
||||||
|
return {
|
||||||
|
id: uri.codeUri.fsPath,
|
||||||
|
type: MentionType.FOLDER,
|
||||||
|
text: uri.displayName,
|
||||||
|
value: uri.codeUri.fsPath,
|
||||||
|
description: relativePath?.root ? relativePath.path : '',
|
||||||
|
contextId: uri.codeUri.fsPath,
|
||||||
|
icon: getIcon('folder'),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const rootUris = (await workspaceService.roots).map((root) => new URI(root.uri).codeUri.fsPath);
|
||||||
|
const results = await searchService.find(searchText, {
|
||||||
|
rootUris,
|
||||||
|
useGitIgnore: true,
|
||||||
|
noIgnoreParent: true,
|
||||||
|
fuzzyMatch: true,
|
||||||
|
limit: 10,
|
||||||
|
onlyFolders: true,
|
||||||
|
});
|
||||||
|
folders = await Promise.all(
|
||||||
|
results.map(async (folder) => {
|
||||||
|
const uri = new URI(folder);
|
||||||
|
return {
|
||||||
|
id: uri.codeUri.fsPath,
|
||||||
|
type: MentionType.FOLDER,
|
||||||
|
text: uri.displayName,
|
||||||
|
value: uri.codeUri.fsPath,
|
||||||
|
description: (await workspaceService.asRelativePath(uri.parent))?.path || '',
|
||||||
|
contextId: uri.codeUri.fsPath,
|
||||||
|
icon: getIcon('folder'),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return folders.filter((folder) => folder.id !== new URI(workspaceService.workspace?.uri).codeUri.fsPath);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultMentionInputFooterOptions: FooterConfig = useMemo(
|
||||||
|
() => ({
|
||||||
|
modelOptions: [
|
||||||
|
{ label: 'QWQ 32B', value: 'qwq-32b' },
|
||||||
|
{ label: 'DeepSeek R1', value: 'deepseek-r1' },
|
||||||
|
],
|
||||||
|
defaultModel: 'deepseek-r1',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
id: 'mcp-server',
|
||||||
|
icon: 'mcp',
|
||||||
|
title: 'MCP Server',
|
||||||
|
onClick: handleShowMCPConfig,
|
||||||
|
position: FooterButtonPosition.LEFT,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showModelSelector: true,
|
||||||
|
}),
|
||||||
|
[handleShowMCPConfig],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleStop = useCallback(() => {
|
||||||
|
aiChatService.cancelRequest();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSend = useCallback(
|
||||||
|
async (content: string, option?: { model: string; [key: string]: any }) => {
|
||||||
|
if (disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSend(content, undefined, undefined, option);
|
||||||
|
},
|
||||||
|
[onSend, editorService, disabled],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.chat_input_container}>
|
||||||
|
<MentionInput
|
||||||
|
mentionItems={defaultMenuItems}
|
||||||
|
onSend={handleSend}
|
||||||
|
onStop={handleStop}
|
||||||
|
loading={disabled}
|
||||||
|
labelService={labelService}
|
||||||
|
workspaceService={workspaceService}
|
||||||
|
placeholder={localize('aiNative.chat.input.placeholder.default')}
|
||||||
|
footerConfig={defaultMentionInputFooterOptions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,7 +1,6 @@
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useInjectable } from '@opensumi/ide-core-browser';
|
import { useInjectable } from '@opensumi/ide-core-browser';
|
||||||
import { Icon, getIcon } from '@opensumi/ide-core-browser/lib/components';
|
|
||||||
import { EnhanceIcon, Thumbs } from '@opensumi/ide-core-browser/lib/components/ai-native';
|
import { EnhanceIcon, Thumbs } from '@opensumi/ide-core-browser/lib/components/ai-native';
|
||||||
import { Progress } from '@opensumi/ide-core-browser/lib/progress/progress-bar';
|
import { Progress } from '@opensumi/ide-core-browser/lib/progress/progress-bar';
|
||||||
import { ChatRenderRegistryToken, isUndefined, localize } from '@opensumi/ide-core-common';
|
import { ChatRenderRegistryToken, isUndefined, localize } from '@opensumi/ide-core-common';
|
||||||
|
@ -13,7 +12,7 @@ import { ChatRenderRegistry } from '../chat/chat.render.registry';
|
||||||
import styles from './components.module.less';
|
import styles from './components.module.less';
|
||||||
|
|
||||||
interface ITinkingProps {
|
interface ITinkingProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode | string | React.ReactNode[];
|
||||||
hasMessage?: boolean;
|
hasMessage?: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
onRegenerate?: () => void;
|
onRegenerate?: () => void;
|
||||||
|
@ -32,8 +31,15 @@ export const ChatThinking = (props: ITinkingProps) => {
|
||||||
[chatRenderRegistry, chatRenderRegistry.chatThinkingRender],
|
[chatRenderRegistry, chatRenderRegistry.chatThinkingRender],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isEmptyChildren = useMemo(() => {
|
||||||
|
if (Array.isArray(children)) {
|
||||||
|
return children.length === 0;
|
||||||
|
}
|
||||||
|
return !children;
|
||||||
|
}, [children]);
|
||||||
|
|
||||||
const renderContent = useCallback(() => {
|
const renderContent = useCallback(() => {
|
||||||
if (!children) {
|
if (isEmptyChildren) {
|
||||||
if (CustomThinkingRender) {
|
if (CustomThinkingRender) {
|
||||||
return <CustomThinkingRender thinkingText={thinkingText} />;
|
return <CustomThinkingRender thinkingText={thinkingText} />;
|
||||||
}
|
}
|
||||||
|
@ -52,7 +58,7 @@ export const ChatThinking = (props: ITinkingProps) => {
|
||||||
{!CustomThinkingRender && (
|
{!CustomThinkingRender && (
|
||||||
<span className={styles.progress_bar}>
|
<span className={styles.progress_bar}>
|
||||||
{/* 保持动画效果一致 */}
|
{/* 保持动画效果一致 */}
|
||||||
{!children && <Progress loading={true} wrapperClassName={styles.ai_native_progress_wrapper} />}
|
{isEmptyChildren && <Progress loading={true} wrapperClassName={styles.ai_native_progress_wrapper} />}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{/* {showStop && (
|
{/* {showStop && (
|
||||||
|
|
|
@ -103,6 +103,8 @@
|
||||||
.fileStats {
|
.fileStats {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { WorkbenchEditorService } from '@opensumi/ide-editor/lib/browser/types';
|
||||||
|
|
||||||
import { FileContext, LLMContextService, LLMContextServiceToken } from '../../../common/llm-context';
|
import { FileContext, LLMContextService, LLMContextServiceToken } from '../../../common/llm-context';
|
||||||
|
|
||||||
import { ContextSelector } from './ContextSelector';
|
import { ContextSelector } from './context-selector';
|
||||||
import styles from './style.module.less';
|
import styles from './style.module.less';
|
||||||
|
|
||||||
const getCollapsedHeight = () => ({ height: 0, opacity: 0 });
|
const getCollapsedHeight = () => ({ height: 0, opacity: 0 });
|
|
@ -1,4 +1,4 @@
|
||||||
.dm-chat-history-header {
|
.chat_history_header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
.dm-chat-history-header-title {
|
.chat_history_header_title {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -22,23 +22,35 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dm-chat-history-header-actions {
|
.chat_history_header_actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
.dm-chat-history-header-actions-history {
|
.chat_history_header_actions_history {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dm-chat-history-header-actions-new {
|
.chat_history_header_actions_new {
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
.kt-popover-title {
|
.kt-popover-title {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
.kt-popover-content {
|
||||||
|
background-color: var(--editor-background);
|
||||||
|
color: var(--editor-foreground);
|
||||||
|
}
|
||||||
|
.kt-popover-arrow {
|
||||||
|
border-bottom-color: var(--editor-background);
|
||||||
|
}
|
||||||
|
.kt-popover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.kt-popover {
|
.kt-popover {
|
||||||
|
@ -75,7 +87,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dm-chat-history-list {
|
.chat_history_search {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat_history_list {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
@ -83,12 +100,12 @@
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dm-chat-history-time {
|
.chat_history_time {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dm-chat-history-item {
|
.chat_history_item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -97,7 +114,7 @@
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
||||||
.dm-chat-history-item-content {
|
.chat_history_item_content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -105,28 +122,28 @@
|
||||||
height: 24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dm-chat-history-item-title {
|
.chat_history_item_title {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dm-chat-history-item-actions {
|
.chat_history_item_actions {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dm-chat-history-item-selected {
|
.chat_history_item_selected {
|
||||||
background: var(--textPreformat-background);
|
background: var(--textPreformat-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--textPreformat-background);
|
background: var(--textPreformat-background);
|
||||||
|
|
||||||
.dm-chat-history-item-actions {
|
.chat_history_item_actions {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.dm-chat-history-item-content {
|
.chat_history_item_content {
|
||||||
max-width: calc(100% - 50px);
|
max-width: calc(100% - 50px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
.stop {
|
.stop {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -38px;
|
bottom: -15px;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
left: -8px;
|
left: -9px;
|
||||||
width: 105%;
|
width: 105%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -453,7 +453,13 @@
|
||||||
h3,
|
h3,
|
||||||
h4,
|
h4,
|
||||||
h5 {
|
h5 {
|
||||||
color: #fff;
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border-bottom: 0;
|
||||||
|
opacity: 0.3;
|
||||||
|
border-color: var(--descriptionForeground);
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
@ -519,6 +525,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mcp_desc {
|
.mcp_desc {
|
||||||
|
@ -565,3 +572,32 @@
|
||||||
color: var(--descriptionForeground);
|
color: var(--descriptionForeground);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attachment {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 4px;
|
||||||
|
margin: 0 2px;
|
||||||
|
background: var(--badge-background);
|
||||||
|
color: var(--badge-foreground);
|
||||||
|
border-radius: 3px;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
:global {
|
||||||
|
.kt-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--chat-slashCommandBackground);
|
||||||
|
color: var(--chat-slashCommandForeground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment_text {
|
||||||
|
line-height: 20px;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,333 @@
|
||||||
|
.input_container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.model_selector {
|
||||||
|
margin-right: 5px;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor_area {
|
||||||
|
position: relative;
|
||||||
|
padding: 0 15px;
|
||||||
|
min-height: 42px;
|
||||||
|
max-height: 105px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
width: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
min-height: 24px;
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
|
.mention_tag {
|
||||||
|
margin: 0 2px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor:empty:before,
|
||||||
|
.editor[data-content=''] + .editor:before {
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
color: var(--descriptionForeground);
|
||||||
|
pointer-events: none;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai_enhance_icon {
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px 3px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--design-text-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--badge-background);
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--badge-foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
&::before {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 12px 15px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.left_control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-direction: row;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right_control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.send_logo,
|
||||||
|
.stop_logo {
|
||||||
|
background-color: var(--badge-background);
|
||||||
|
color: var(--badge-foreground);
|
||||||
|
.stop_logo_icon {
|
||||||
|
line-height: 16px;
|
||||||
|
color: var(--kt-dangerButton-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.send_logo {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--kt-primaryButton-background);
|
||||||
|
|
||||||
|
.send_logo_icon {
|
||||||
|
color: var(--kt-primaryButton-foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.send_button {
|
||||||
|
color: var(--icon-foreground);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send_button:hover {
|
||||||
|
background-color: var(--notification-success-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention_panel_container {
|
||||||
|
position: absolute;
|
||||||
|
top: -20px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
transform: translateY(-100%);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention_panel {
|
||||||
|
background-color: var(--editor-background);
|
||||||
|
color: var(--editor-foreground);
|
||||||
|
border-radius: 6px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 8px 4px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention_item {
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 22px;
|
||||||
|
height: 22px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: var(--foreground);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention_list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention_item.active {
|
||||||
|
color: var(--kt-tree-inactiveSelectionForeground);
|
||||||
|
background: var(--kt-tree-inactiveSelectionBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention_item:hover:not(.active) {
|
||||||
|
color: var(--kt-tree-inactiveSelectionForeground);
|
||||||
|
background: var(--kt-tree-inactiveSelectionBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention_item_left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention_item_icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention_item_text {
|
||||||
|
font-size: 13px;
|
||||||
|
margin-right: 6px;
|
||||||
|
display: inline;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention_item_description {
|
||||||
|
color: var(--descriptionForeground);
|
||||||
|
font-size: 13px;
|
||||||
|
display: inline;
|
||||||
|
flex: 1;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: 5px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention_item_right {
|
||||||
|
color: #8b949e;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention_panel_title {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back_button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back_button:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention_tag {
|
||||||
|
background-color: var(--chat-slashCommandBackground);
|
||||||
|
color: var(--chat-slashCommandForeground);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 4px;
|
||||||
|
margin: 0 3px;
|
||||||
|
user-select: all;
|
||||||
|
cursor: default;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention_icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty_state {
|
||||||
|
padding: 5px 10px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--descriptionForeground);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model_selector {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp_logo {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send_logo {
|
||||||
|
color: var(--icon-foreground);
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: auto;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--notification-success-foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover_icon {
|
||||||
|
// 移除 margin-left: auto
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading_container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading_bar {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 2px;
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, var(--kt-primaryButton-background), transparent);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: loading-bar 1.5s infinite;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading-bar {
|
||||||
|
0% {
|
||||||
|
background-position: 100% 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: -100% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no_results {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
|
@ -0,0 +1,952 @@
|
||||||
|
import cls from 'classnames';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { Popover, PopoverPosition, Select, getIcon } from '@opensumi/ide-core-browser/lib/components';
|
||||||
|
import { EnhanceIcon } from '@opensumi/ide-core-browser/lib/components/ai-native';
|
||||||
|
import { URI } from '@opensumi/ide-utils';
|
||||||
|
|
||||||
|
import styles from './mention-input.module.less';
|
||||||
|
import { MentionPanel } from './mention-panel';
|
||||||
|
import { FooterButtonPosition, MENTION_KEYWORD, MentionInputProps, MentionItem, MentionState } from './types';
|
||||||
|
|
||||||
|
export const WHITE_SPACE_TEXT = ' ';
|
||||||
|
|
||||||
|
export const MentionInput: React.FC<MentionInputProps> = ({
|
||||||
|
mentionItems = [],
|
||||||
|
onSend,
|
||||||
|
onStop,
|
||||||
|
loading = false,
|
||||||
|
mentionKeyword = MENTION_KEYWORD,
|
||||||
|
onSelectionChange,
|
||||||
|
labelService,
|
||||||
|
workspaceService,
|
||||||
|
placeholder = 'Ask anything, @ to mention',
|
||||||
|
footerConfig = {
|
||||||
|
buttons: [],
|
||||||
|
showModelSelector: false,
|
||||||
|
},
|
||||||
|
}) => {
|
||||||
|
const editorRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const [mentionState, setMentionState] = React.useState<MentionState>({
|
||||||
|
active: false,
|
||||||
|
startPos: null,
|
||||||
|
filter: '',
|
||||||
|
position: { top: 0, left: 0 },
|
||||||
|
activeIndex: 0,
|
||||||
|
level: 0, // 0: 一级菜单, 1: 二级菜单
|
||||||
|
parentType: null, // 二级菜单的父类型
|
||||||
|
secondLevelFilter: '', // 二级菜单的筛选文本
|
||||||
|
inlineSearchActive: false, // 是否在输入框中进行二级搜索
|
||||||
|
inlineSearchStartPos: null, // 内联搜索的起始位置
|
||||||
|
loading: false, // 添加加载状态
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加模型选择状态
|
||||||
|
const [selectedModel, setSelectedModel] = React.useState<string>(footerConfig.defaultModel || '');
|
||||||
|
|
||||||
|
// 添加缓存状态,用于存储二级菜单项
|
||||||
|
const [secondLevelCache, setSecondLevelCache] = React.useState<Record<string, MentionItem[]>>({});
|
||||||
|
|
||||||
|
// 添加历史记录状态
|
||||||
|
const [history, setHistory] = React.useState<string[]>([]);
|
||||||
|
const [historyIndex, setHistoryIndex] = React.useState<number>(-1);
|
||||||
|
const [currentInput, setCurrentInput] = React.useState<string>('');
|
||||||
|
const [isNavigatingHistory, setIsNavigatingHistory] = React.useState<boolean>(false);
|
||||||
|
|
||||||
|
// 获取当前菜单项
|
||||||
|
const getCurrentItems = (): MentionItem[] => {
|
||||||
|
if (mentionState.level === 0) {
|
||||||
|
return mentionItems;
|
||||||
|
} else if (mentionState.parentType) {
|
||||||
|
// 如果正在加载,返回缓存的项目
|
||||||
|
if (mentionState.loading) {
|
||||||
|
return secondLevelCache[mentionState.parentType] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回缓存的项目
|
||||||
|
return secondLevelCache[mentionState.parentType] || [];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加防抖函数
|
||||||
|
const useDebounce = <T,>(value: T, delay: number): T => {
|
||||||
|
const [debouncedValue, setDebouncedValue] = React.useState<T>(value);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedValue(value);
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [value, delay]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用防抖处理搜索文本
|
||||||
|
const debouncedSecondLevelFilter = useDebounce(mentionState.secondLevelFilter, 300);
|
||||||
|
|
||||||
|
// 监听搜索文本变化,实时更新二级菜单
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (mentionState.level === 1 && mentionState.parentType && debouncedSecondLevelFilter !== undefined) {
|
||||||
|
// 查找父级菜单项
|
||||||
|
const parentItem = mentionItems.find((item) => item.id === mentionState.parentType);
|
||||||
|
if (!parentItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置加载状态
|
||||||
|
setMentionState((prev) => ({ ...prev, loading: true }));
|
||||||
|
|
||||||
|
// 异步加载
|
||||||
|
const fetchItems = async () => {
|
||||||
|
try {
|
||||||
|
// 首先显示高优先级项目(如果有)
|
||||||
|
const items: MentionItem[] = [];
|
||||||
|
if (parentItem.getHighestLevelItems) {
|
||||||
|
const highestLevelItems = parentItem.getHighestLevelItems();
|
||||||
|
for (const item of highestLevelItems) {
|
||||||
|
if (!items.some((i) => i.id === item.id)) {
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 立即更新缓存,显示高优先级项目
|
||||||
|
setSecondLevelCache((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[mentionState.parentType!]: highestLevelItems,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 然后异步加载更多项目
|
||||||
|
if (parentItem.getItems) {
|
||||||
|
try {
|
||||||
|
// 获取子菜单项
|
||||||
|
const newItems = await parentItem.getItems(debouncedSecondLevelFilter);
|
||||||
|
|
||||||
|
// 去重合并
|
||||||
|
const combinedItems: MentionItem[] = [...items];
|
||||||
|
|
||||||
|
for (const item of newItems) {
|
||||||
|
if (!combinedItems.some((i) => i.id === item.id)) {
|
||||||
|
combinedItems.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
setSecondLevelCache((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[mentionState.parentType!]: combinedItems,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
// 如果异步加载失败,至少保留高优先级项目
|
||||||
|
setMentionState((prev) => ({ ...prev, loading: false }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后清除加载状态
|
||||||
|
setMentionState((prev) => ({ ...prev, loading: false }));
|
||||||
|
} catch (error) {
|
||||||
|
setMentionState((prev) => ({ ...prev, loading: false }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchItems();
|
||||||
|
}
|
||||||
|
}, [debouncedSecondLevelFilter, mentionState.level, mentionState.parentType]);
|
||||||
|
|
||||||
|
// 获取光标位置
|
||||||
|
const getCursorPosition = (element: HTMLElement): number => {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (!selection || !selection.rangeCount) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
const preCaretRange = range.cloneRange();
|
||||||
|
preCaretRange.selectNodeContents(element);
|
||||||
|
preCaretRange.setEnd(range.endContainer, range.endOffset);
|
||||||
|
return preCaretRange.toString().length;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理输入事件
|
||||||
|
const handleInput = () => {
|
||||||
|
// 如果用户开始输入,退出历史导航模式
|
||||||
|
if (isNavigatingHistory) {
|
||||||
|
setIsNavigatingHistory(false);
|
||||||
|
setHistoryIndex(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (!selection || !selection.rangeCount || !editorRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = editorRef.current.textContent || '';
|
||||||
|
const cursorPos = getCursorPosition(editorRef.current);
|
||||||
|
|
||||||
|
// 判断是否刚输入了 @
|
||||||
|
if (text[cursorPos - 1] === mentionKeyword && !mentionState.active && !mentionState.inlineSearchActive) {
|
||||||
|
setMentionState({
|
||||||
|
active: true,
|
||||||
|
startPos: cursorPos,
|
||||||
|
filter: mentionKeyword,
|
||||||
|
position: { top: 0, left: 0 }, // 固定位置,不再需要动态计算
|
||||||
|
activeIndex: 0,
|
||||||
|
level: 0,
|
||||||
|
parentType: null,
|
||||||
|
secondLevelFilter: '',
|
||||||
|
inlineSearchActive: false,
|
||||||
|
inlineSearchStartPos: null,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已激活提及面板且在一级菜单,更新过滤内容
|
||||||
|
if (mentionState.active && mentionState.level === 0 && mentionState.startPos !== null) {
|
||||||
|
if (cursorPos < mentionState.startPos) {
|
||||||
|
// 如果光标移到了 @ 之前,关闭面板
|
||||||
|
setMentionState((prev) => ({ ...prev, active: false }));
|
||||||
|
} else {
|
||||||
|
const newFilter = text.substring(mentionState.startPos - 1, cursorPos);
|
||||||
|
setMentionState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
filter: newFilter,
|
||||||
|
activeIndex: 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果在输入框中进行二级搜索
|
||||||
|
if (mentionState.inlineSearchActive && mentionState.inlineSearchStartPos !== null && mentionState.parentType) {
|
||||||
|
// 获取父级类型
|
||||||
|
const parentItem = mentionItems.find((i) => i.id === mentionState.parentType);
|
||||||
|
if (!parentItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查光标是否在 @type: 之后
|
||||||
|
const typePrefix = `@${parentItem.type}:`;
|
||||||
|
const prefixPos = mentionState.inlineSearchStartPos - typePrefix.length;
|
||||||
|
|
||||||
|
if (prefixPos >= 0 && cursorPos > prefixPos + typePrefix.length) {
|
||||||
|
// 提取搜索文本
|
||||||
|
const searchText = text.substring(prefixPos + typePrefix.length, cursorPos);
|
||||||
|
|
||||||
|
// 只有当搜索文本变化时才更新状态
|
||||||
|
if (searchText !== mentionState.secondLevelFilter) {
|
||||||
|
setMentionState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
secondLevelFilter: searchText,
|
||||||
|
active: true,
|
||||||
|
activeIndex: 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else if (cursorPos <= prefixPos) {
|
||||||
|
// 如果光标移到了 @type: 之前,关闭内联搜索
|
||||||
|
setMentionState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
inlineSearchActive: false,
|
||||||
|
active: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查输入框高度,如果超过最大高度则添加滚动条
|
||||||
|
if (editorRef.current) {
|
||||||
|
const editorHeight = editorRef.current.scrollHeight;
|
||||||
|
if (editorHeight > 120) {
|
||||||
|
editorRef.current.style.overflowY = 'auto';
|
||||||
|
} else {
|
||||||
|
editorRef.current.style.overflowY = 'hidden';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查编辑器内容,处理只有 <br> 标签的情况
|
||||||
|
if (editorRef.current) {
|
||||||
|
const content = editorRef.current.innerHTML;
|
||||||
|
// 如果内容为空或只有 <br> 标签
|
||||||
|
if (content === '' || content === '<br>' || content === '<br/>') {
|
||||||
|
// 清空编辑器内容
|
||||||
|
editorRef.current.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理键盘事件
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
// 如果按下ESC键且提及面板处于活动状态或内联搜索处于活动状态
|
||||||
|
if (e.key === 'Escape' && (mentionState.active || mentionState.inlineSearchActive)) {
|
||||||
|
// 如果在二级菜单,返回一级菜单
|
||||||
|
if (mentionState.level > 0) {
|
||||||
|
setMentionState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
level: 0,
|
||||||
|
activeIndex: 0,
|
||||||
|
secondLevelFilter: '',
|
||||||
|
inlineSearchActive: false,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// 如果在一级菜单,完全关闭面板
|
||||||
|
setMentionState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
active: false,
|
||||||
|
inlineSearchActive: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加对 @ 键的监听,支持在任意位置触发菜单
|
||||||
|
if (e.key === MENTION_KEYWORD && !mentionState.active && !mentionState.inlineSearchActive && editorRef.current) {
|
||||||
|
const cursorPos = getCursorPosition(editorRef.current);
|
||||||
|
|
||||||
|
// 立即设置菜单状态,不等待 handleInput
|
||||||
|
setMentionState({
|
||||||
|
active: true,
|
||||||
|
startPos: cursorPos + 1, // +1 因为 @ 还没有被插入
|
||||||
|
filter: mentionKeyword,
|
||||||
|
position: { top: 0, left: 0 }, // 固定位置
|
||||||
|
activeIndex: 0,
|
||||||
|
level: 0,
|
||||||
|
parentType: null,
|
||||||
|
secondLevelFilter: '',
|
||||||
|
inlineSearchActive: false,
|
||||||
|
inlineSearchStartPos: null,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 不要阻止默认行为,让 @ 正常输入到编辑器中
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加对 Enter 键的处理,只有在按下 Shift+Enter 时才允许换行
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
// 检查是否是输入法的回车键
|
||||||
|
// isComposing 属性表示是否正在进行输入法组合输入
|
||||||
|
if (e.nativeEvent.isComposing) {
|
||||||
|
return; // 如果是输入法组合输入过程中的回车,不做任何处理
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!mentionState.active) {
|
||||||
|
handleSend();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理上下方向键导航历史记录
|
||||||
|
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
||||||
|
// 只有在非提及面板激活状态下才处理历史导航
|
||||||
|
if (!mentionState.active && !mentionState.inlineSearchActive && editorRef.current) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// 如果是第一次按上下键,保存当前输入
|
||||||
|
if (!isNavigatingHistory) {
|
||||||
|
setCurrentInput(editorRef.current.innerHTML);
|
||||||
|
setIsNavigatingHistory(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算新的历史索引
|
||||||
|
let newIndex = historyIndex;
|
||||||
|
if (e.key === 'ArrowUp') {
|
||||||
|
// 向上导航到较早的历史记录
|
||||||
|
newIndex = Math.min(history.length - 1, historyIndex + 1);
|
||||||
|
} else {
|
||||||
|
// 向下导航到较新的历史记录
|
||||||
|
newIndex = Math.max(-1, historyIndex - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHistoryIndex(newIndex);
|
||||||
|
|
||||||
|
// 更新编辑器内容
|
||||||
|
if (newIndex === -1) {
|
||||||
|
// 恢复到当前输入
|
||||||
|
editorRef.current.innerHTML = currentInput;
|
||||||
|
} else {
|
||||||
|
// 显示历史记录
|
||||||
|
editorRef.current.innerHTML = history[history.length - 1 - newIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将光标移到末尾
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(editorRef.current);
|
||||||
|
range.collapse(false);
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (selection) {
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (isNavigatingHistory && e.key !== 'ArrowUp' && e.key !== 'ArrowDown') {
|
||||||
|
// 如果用户在浏览历史记录后开始输入,退出历史导航模式
|
||||||
|
setIsNavigatingHistory(false);
|
||||||
|
setHistoryIndex(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果提及面板未激活,不处理其他键盘事件
|
||||||
|
if (!mentionState.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前过滤后的项目
|
||||||
|
let filteredItems = getCurrentItems();
|
||||||
|
|
||||||
|
// 一级菜单过滤
|
||||||
|
if (mentionState.level === 0 && mentionState.filter && mentionState.filter.length > 1) {
|
||||||
|
const searchText = mentionState.filter.substring(1).toLowerCase();
|
||||||
|
filteredItems = filteredItems.filter((item) => item.text.toLowerCase().includes(searchText));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二级菜单过滤已经在 getCurrentItems 中处理
|
||||||
|
|
||||||
|
if (filteredItems.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
// 向下导航
|
||||||
|
setMentionState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
activeIndex: (prev.activeIndex + 1) % filteredItems.length,
|
||||||
|
}));
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
// 向上导航
|
||||||
|
setMentionState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
activeIndex: (prev.activeIndex - 1 + filteredItems.length) % filteredItems.length,
|
||||||
|
}));
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
||||||
|
// 确认选择
|
||||||
|
if (filteredItems.length > 0) {
|
||||||
|
handleSelectItem(filteredItems[mentionState.activeIndex]);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 Backspace 键,检查是否需要清空编辑器
|
||||||
|
if (e.key === 'Backspace' && editorRef.current) {
|
||||||
|
const content = editorRef.current.innerHTML;
|
||||||
|
if (content === '<br>' || content === '<br/>') {
|
||||||
|
editorRef.current.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加对输入法事件的处理
|
||||||
|
const handleCompositionEnd = () => {
|
||||||
|
// 输入法输入完成后的处理
|
||||||
|
// 这里可以添加额外的逻辑,如果需要的话
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化编辑器
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
// 设置初始占位符
|
||||||
|
if (placeholder && !editorRef.current.textContent) {
|
||||||
|
editorRef.current.setAttribute('data-placeholder', placeholder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [placeholder]);
|
||||||
|
|
||||||
|
// 处理点击事件
|
||||||
|
const handleDocumentClick = (e: MouseEvent) => {
|
||||||
|
if (mentionState.active && !document.querySelector(`.${styles.mention_panel}`)?.contains(e.target as Node)) {
|
||||||
|
setMentionState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
active: false,
|
||||||
|
inlineSearchActive: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加和移除全局点击事件监听器
|
||||||
|
React.useEffect(() => {
|
||||||
|
document.addEventListener('click', handleDocumentClick, true);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('click', handleDocumentClick, true);
|
||||||
|
};
|
||||||
|
}, [mentionState.active]);
|
||||||
|
|
||||||
|
// 选择提及项目
|
||||||
|
const handleSelectItem = (item: MentionItem, isTriggerByClick = false) => {
|
||||||
|
if (!editorRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果项目有子菜单,进入二级菜单
|
||||||
|
if (item.getItems) {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (!selection || !selection.rangeCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是从一级菜单选择了带子菜单的项目
|
||||||
|
if (mentionState.level === 0 && mentionState.startPos !== null) {
|
||||||
|
// 更安全地处理文本替换
|
||||||
|
let textNode;
|
||||||
|
let startOffset;
|
||||||
|
let endOffset;
|
||||||
|
|
||||||
|
// 找到包含 @ 符号的文本节点
|
||||||
|
const walker = document.createTreeWalker(editorRef.current, NodeFilter.SHOW_TEXT);
|
||||||
|
let charCount = 0;
|
||||||
|
let node;
|
||||||
|
|
||||||
|
while ((node = walker.nextNode())) {
|
||||||
|
const nodeLength = node.textContent?.length || 0;
|
||||||
|
|
||||||
|
// 检查 @ 符号是否在这个节点中
|
||||||
|
if (mentionState.startPos - 1 >= charCount && mentionState.startPos - 1 < charCount + nodeLength) {
|
||||||
|
textNode = node;
|
||||||
|
startOffset = mentionState.startPos - 1 - charCount;
|
||||||
|
|
||||||
|
// 确保不会超出节点范围
|
||||||
|
const cursorPos = isTriggerByClick
|
||||||
|
? mentionState.startPos + mentionState.filter.length - 1
|
||||||
|
: getCursorPosition(editorRef.current);
|
||||||
|
endOffset = Math.min(cursorPos - charCount, nodeLength);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
charCount += nodeLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textNode) {
|
||||||
|
// 创建一个新的范围来替换文本
|
||||||
|
const tempRange = document.createRange();
|
||||||
|
tempRange.setStart(textNode, startOffset);
|
||||||
|
tempRange.setEnd(textNode, endOffset);
|
||||||
|
|
||||||
|
// 替换为 @type:
|
||||||
|
tempRange.deleteContents();
|
||||||
|
const typePrefix = document.createTextNode(`${mentionKeyword}${item.type}:`);
|
||||||
|
tempRange.insertNode(typePrefix);
|
||||||
|
|
||||||
|
// 将光标移到 @type: 后面
|
||||||
|
const newRange = document.createRange();
|
||||||
|
newRange.setStartAfter(typePrefix);
|
||||||
|
newRange.setEndAfter(typePrefix);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(newRange);
|
||||||
|
// 激活内联搜索模式
|
||||||
|
setMentionState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
active: true,
|
||||||
|
level: 1,
|
||||||
|
parentType: item.id,
|
||||||
|
inlineSearchActive: true,
|
||||||
|
inlineSearchStartPos: getCursorPosition(editorRef.current as HTMLElement),
|
||||||
|
secondLevelFilter: '',
|
||||||
|
activeIndex: 0,
|
||||||
|
}));
|
||||||
|
editorRef.current.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (!selection || !selection.rangeCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是在内联搜索模式下选择项目
|
||||||
|
if (mentionState.inlineSearchActive && mentionState.parentType && mentionState.inlineSearchStartPos !== null) {
|
||||||
|
// 找到 @type: 的位置
|
||||||
|
const parentItem = mentionItems.find((i) => i.id === mentionState.parentType);
|
||||||
|
if (!parentItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typePrefix = `${mentionKeyword}${parentItem.type}:`;
|
||||||
|
const prefixPos = mentionState.inlineSearchStartPos - typePrefix.length;
|
||||||
|
|
||||||
|
if (prefixPos >= 0) {
|
||||||
|
// 创建一个带样式的提及标签
|
||||||
|
const mentionTag = document.createElement('span');
|
||||||
|
mentionTag.className = styles.mention_tag;
|
||||||
|
mentionTag.dataset.id = item.id;
|
||||||
|
mentionTag.dataset.type = item.type;
|
||||||
|
mentionTag.dataset.contextId = item.contextId || '';
|
||||||
|
mentionTag.contentEditable = 'false';
|
||||||
|
|
||||||
|
// 为 file 和 folder 类型添加图标
|
||||||
|
if (item.type === 'file' || item.type === 'folder') {
|
||||||
|
// 创建图标容器
|
||||||
|
const iconSpan = document.createElement('span');
|
||||||
|
iconSpan.className = cls(
|
||||||
|
styles.mention_icon,
|
||||||
|
item.type === 'file' ? labelService?.getIcon(new URI(item.text)) : getIcon('folder'),
|
||||||
|
);
|
||||||
|
mentionTag.appendChild(iconSpan);
|
||||||
|
}
|
||||||
|
const workspace = workspaceService?.workspace;
|
||||||
|
let relativePath = item.text;
|
||||||
|
if (workspace && item.contextId) {
|
||||||
|
relativePath = item.contextId.replace(new URI(workspace.uri).codeUri.fsPath, '').slice(1);
|
||||||
|
}
|
||||||
|
// 创建文本内容容器
|
||||||
|
const textSpan = document.createTextNode(relativePath);
|
||||||
|
mentionTag.appendChild(textSpan);
|
||||||
|
|
||||||
|
// 创建一个范围从 @type: 开始到当前光标
|
||||||
|
const tempRange = document.createRange();
|
||||||
|
|
||||||
|
// 定位到 @type: 的位置
|
||||||
|
let charIndex = 0;
|
||||||
|
let foundStart = false;
|
||||||
|
const textNodes: Array<{ node: Node; start: number; end: number }> = [];
|
||||||
|
|
||||||
|
function findPosition(node: Node) {
|
||||||
|
if (node.nodeType === 3) {
|
||||||
|
// 文本节点
|
||||||
|
textNodes.push({
|
||||||
|
node,
|
||||||
|
start: charIndex,
|
||||||
|
end: charIndex + node.textContent!.length,
|
||||||
|
});
|
||||||
|
charIndex += node.textContent!.length;
|
||||||
|
} else if (node.nodeType === 1) {
|
||||||
|
// 元素节点
|
||||||
|
const children = node.childNodes || [];
|
||||||
|
for (const child of Array.from(children)) {
|
||||||
|
findPosition(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findPosition(editorRef.current);
|
||||||
|
|
||||||
|
for (const textNode of textNodes) {
|
||||||
|
if (prefixPos >= textNode.start && prefixPos <= textNode.end) {
|
||||||
|
const startOffset = prefixPos - textNode.start;
|
||||||
|
tempRange.setStart(textNode.node, startOffset);
|
||||||
|
foundStart = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundStart) {
|
||||||
|
// 如果是点击触发,使用过滤文本的长度来确定结束位置
|
||||||
|
const cursorPos = isTriggerByClick
|
||||||
|
? prefixPos + typePrefix.length + mentionState.secondLevelFilter.length
|
||||||
|
: getCursorPosition(editorRef.current);
|
||||||
|
|
||||||
|
if (cursorPos >= textNode.start && cursorPos <= textNode.end) {
|
||||||
|
const endOffset = cursorPos - textNode.start;
|
||||||
|
tempRange.setEnd(textNode.node, endOffset);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundStart) {
|
||||||
|
tempRange.deleteContents();
|
||||||
|
tempRange.insertNode(mentionTag);
|
||||||
|
|
||||||
|
// 将光标移到提及标签后面
|
||||||
|
const newRange = document.createRange();
|
||||||
|
newRange.setStartAfter(mentionTag);
|
||||||
|
newRange.setEndAfter(mentionTag);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(newRange);
|
||||||
|
|
||||||
|
// 添加一个空格,增加间隔
|
||||||
|
const spaceNode = document.createTextNode('\u00A0'); // 使用不间断空格
|
||||||
|
newRange.insertNode(spaceNode);
|
||||||
|
newRange.setStartAfter(spaceNode);
|
||||||
|
newRange.setEndAfter(spaceNode);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(newRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
setMentionState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
active: false,
|
||||||
|
inlineSearchActive: false,
|
||||||
|
}));
|
||||||
|
editorRef.current.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原有的处理逻辑(用于非内联搜索情况)
|
||||||
|
// 创建一个带样式的提及标签
|
||||||
|
const mentionTag = document.createElement('span');
|
||||||
|
mentionTag.className = styles.mention_tag;
|
||||||
|
mentionTag.dataset.id = item.id;
|
||||||
|
mentionTag.dataset.type = item.type;
|
||||||
|
mentionTag.dataset.contextId = item.contextId || '';
|
||||||
|
mentionTag.contentEditable = 'false';
|
||||||
|
|
||||||
|
// 为 file 和 folder 类型添加图标
|
||||||
|
if (item.type === 'file' || item.type === 'folder') {
|
||||||
|
// 创建图标容器
|
||||||
|
const iconSpan = document.createElement('span');
|
||||||
|
iconSpan.className = cls(
|
||||||
|
styles.mention_icon,
|
||||||
|
item.type === 'file' ? labelService?.getIcon(new URI(item.text)) : getIcon('folder'),
|
||||||
|
);
|
||||||
|
mentionTag.appendChild(iconSpan);
|
||||||
|
}
|
||||||
|
const workspace = workspaceService?.workspace;
|
||||||
|
let relativePath = item.text;
|
||||||
|
if (workspace && item.contextId) {
|
||||||
|
relativePath = item.contextId.replace(new URI(workspace.uri).codeUri.fsPath, '').slice(1);
|
||||||
|
}
|
||||||
|
// 创建文本内容容器
|
||||||
|
const textSpan = document.createTextNode(relativePath);
|
||||||
|
mentionTag.appendChild(textSpan);
|
||||||
|
|
||||||
|
// 定位到 @ 符号的位置
|
||||||
|
let charIndex = 0;
|
||||||
|
let foundStart = false;
|
||||||
|
const textNodes: Array<{ node: Node; start: number; end: number }> = [];
|
||||||
|
|
||||||
|
function findPosition(node: Node) {
|
||||||
|
if (node.nodeType === 3) {
|
||||||
|
// 文本节点
|
||||||
|
textNodes.push({
|
||||||
|
node,
|
||||||
|
start: charIndex,
|
||||||
|
end: charIndex + node.textContent!.length,
|
||||||
|
});
|
||||||
|
charIndex += node.textContent!.length;
|
||||||
|
} else if (node.nodeType === 1) {
|
||||||
|
// 元素节点
|
||||||
|
const children = node.childNodes;
|
||||||
|
for (const child of Array.from(children)) {
|
||||||
|
findPosition(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findPosition(editorRef.current);
|
||||||
|
|
||||||
|
const tempRange = document.createRange();
|
||||||
|
|
||||||
|
if (mentionState.startPos !== null) {
|
||||||
|
for (const textNode of textNodes) {
|
||||||
|
if (mentionState.startPos - 1 >= textNode.start && mentionState.startPos - 1 <= textNode.end) {
|
||||||
|
const startOffset = mentionState.startPos - 1 - textNode.start;
|
||||||
|
tempRange.setStart(textNode.node, startOffset);
|
||||||
|
foundStart = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundStart) {
|
||||||
|
// 如果是点击触发,使用过滤文本的长度来确定结束位置
|
||||||
|
const cursorPos = isTriggerByClick
|
||||||
|
? mentionState.startPos + mentionState.filter.length - 1
|
||||||
|
: getCursorPosition(editorRef.current);
|
||||||
|
|
||||||
|
if (cursorPos >= textNode.start && cursorPos <= textNode.end) {
|
||||||
|
const endOffset = cursorPos - textNode.start;
|
||||||
|
tempRange.setEnd(textNode.node, endOffset);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundStart) {
|
||||||
|
tempRange.deleteContents();
|
||||||
|
tempRange.insertNode(mentionTag);
|
||||||
|
|
||||||
|
// 将光标移到提及标签后面
|
||||||
|
const newRange = document.createRange();
|
||||||
|
newRange.setStartAfter(mentionTag);
|
||||||
|
newRange.setEndAfter(mentionTag);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(newRange);
|
||||||
|
|
||||||
|
// 添加一个空格,增加间隔
|
||||||
|
const spaceNode = document.createTextNode('\u00A0'); // 使用不间断空格
|
||||||
|
newRange.insertNode(spaceNode);
|
||||||
|
newRange.setStartAfter(spaceNode);
|
||||||
|
newRange.setEndAfter(spaceNode);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(newRange);
|
||||||
|
}
|
||||||
|
setMentionState((prev) => ({ ...prev, active: false }));
|
||||||
|
editorRef.current.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理模型选择变更
|
||||||
|
const handleModelChange = React.useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
setSelectedModel(value);
|
||||||
|
onSelectionChange?.(value);
|
||||||
|
},
|
||||||
|
[selectedModel, onSelectionChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 修改 handleSend 函数
|
||||||
|
const handleSend = () => {
|
||||||
|
if (!editorRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取原始HTML内容
|
||||||
|
const rawContent = editorRef.current.innerHTML;
|
||||||
|
if (!rawContent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建一个临时元素来处理内容
|
||||||
|
const tempDiv = document.createElement('div');
|
||||||
|
tempDiv.innerHTML = rawContent;
|
||||||
|
|
||||||
|
// 查找所有提及标签并替换为对应的contextId
|
||||||
|
const mentionTags = tempDiv.querySelectorAll(`.${styles.mention_tag}`);
|
||||||
|
mentionTags.forEach((tag) => {
|
||||||
|
const contextId = tag.getAttribute('data-context-id');
|
||||||
|
if (contextId) {
|
||||||
|
// 替换为contextId
|
||||||
|
const replacement = document.createTextNode(
|
||||||
|
`{{${mentionKeyword}${tag.getAttribute('data-type')}:${contextId}}}`,
|
||||||
|
);
|
||||||
|
// 替换内容
|
||||||
|
tag.parentNode?.replaceChild(replacement, tag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取处理后的内容
|
||||||
|
let processedContent = tempDiv.innerHTML;
|
||||||
|
processedContent = processedContent.trim().replaceAll(WHITE_SPACE_TEXT, ' ');
|
||||||
|
// 添加到历史记录
|
||||||
|
if (rawContent) {
|
||||||
|
setHistory((prev) => [...prev, rawContent]);
|
||||||
|
// 重置历史导航状态
|
||||||
|
setHistoryIndex(-1);
|
||||||
|
setIsNavigatingHistory(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onSend) {
|
||||||
|
// 传递当前选择的模型和其他配置信息
|
||||||
|
onSend(processedContent, {
|
||||||
|
model: selectedModel,
|
||||||
|
...footerConfig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
editorRef.current.innerHTML = '';
|
||||||
|
|
||||||
|
// 重置编辑器高度和滚动条
|
||||||
|
if (editorRef.current) {
|
||||||
|
editorRef.current.style.overflowY = 'hidden';
|
||||||
|
editorRef.current.style.height = 'auto';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStop = React.useCallback(() => {
|
||||||
|
if (onStop) {
|
||||||
|
onStop();
|
||||||
|
}
|
||||||
|
}, [onStop]);
|
||||||
|
|
||||||
|
// 渲染自定义按钮
|
||||||
|
const renderButtons = React.useCallback(
|
||||||
|
(position: FooterButtonPosition) =>
|
||||||
|
(footerConfig.buttons || [])
|
||||||
|
.filter((button) => button.position === position)
|
||||||
|
.map((button) => (
|
||||||
|
<Popover
|
||||||
|
key={button.id}
|
||||||
|
overlayClassName={styles.popover_icon}
|
||||||
|
id={`ai-chat-${button.id}`}
|
||||||
|
position={PopoverPosition.top}
|
||||||
|
title={button.title}
|
||||||
|
>
|
||||||
|
<EnhanceIcon
|
||||||
|
className={cls(getIcon(button.icon), styles[`${button.id}_logo`])}
|
||||||
|
tabIndex={0}
|
||||||
|
role='button'
|
||||||
|
ariaLabel={button.title}
|
||||||
|
onClick={button.onClick}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
)),
|
||||||
|
[footerConfig.buttons],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.input_container}>
|
||||||
|
{mentionState.active && (
|
||||||
|
<div className={styles.mention_panel_container}>
|
||||||
|
<MentionPanel
|
||||||
|
items={getCurrentItems()}
|
||||||
|
activeIndex={mentionState.activeIndex}
|
||||||
|
onSelectItem={(item) => handleSelectItem(item, true)}
|
||||||
|
position={{ top: 0, left: 0 }}
|
||||||
|
filter={mentionState.level === 0 ? mentionState.filter : mentionState.secondLevelFilter}
|
||||||
|
visible={true}
|
||||||
|
level={mentionState.level}
|
||||||
|
loading={mentionState.loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.editor_area}>
|
||||||
|
<div
|
||||||
|
ref={editorRef}
|
||||||
|
className={styles.editor}
|
||||||
|
contentEditable={true}
|
||||||
|
onInput={handleInput}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onCompositionEnd={handleCompositionEnd}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<div className={styles.left_control}>
|
||||||
|
{footerConfig.showModelSelector && (
|
||||||
|
<Select
|
||||||
|
options={footerConfig.modelOptions || []}
|
||||||
|
value={selectedModel}
|
||||||
|
onChange={handleModelChange}
|
||||||
|
className={styles.model_selector}
|
||||||
|
size='small'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{renderButtons(FooterButtonPosition.LEFT)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.right_control}>
|
||||||
|
{renderButtons(FooterButtonPosition.RIGHT)}
|
||||||
|
<Popover
|
||||||
|
overlayClassName={styles.popover_icon}
|
||||||
|
id={'ai-chat-send'}
|
||||||
|
position={PopoverPosition.top}
|
||||||
|
content={!loading ? 'Send' : 'Stop'}
|
||||||
|
>
|
||||||
|
{!loading ? (
|
||||||
|
<EnhanceIcon
|
||||||
|
wrapperClassName={styles.send_logo}
|
||||||
|
className={cls(getIcon('send-outlined'), styles.send_logo_icon)}
|
||||||
|
tabIndex={0}
|
||||||
|
role='button'
|
||||||
|
onClick={handleSend}
|
||||||
|
ariaLabel={'Send'}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<EnhanceIcon
|
||||||
|
wrapperClassName={styles.stop_logo}
|
||||||
|
className={cls(getIcon('stop'), styles.stop_logo_icon)}
|
||||||
|
tabIndex={0}
|
||||||
|
role='button'
|
||||||
|
ariaLabel={'Stop'}
|
||||||
|
onClick={handleStop}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,24 @@
|
||||||
|
import cls from 'classnames';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { Icon, getIcon } from '@opensumi/ide-core-browser/lib/components';
|
||||||
|
|
||||||
|
import styles from './mention-input.module.less';
|
||||||
|
import { MentionItem as MentionItemType } from './types';
|
||||||
|
|
||||||
|
interface MentionItemProps {
|
||||||
|
item: MentionItemType;
|
||||||
|
isActive: boolean;
|
||||||
|
onClick: (item: MentionItemType) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MentionItem: React.FC<MentionItemProps> = ({ item, isActive, onClick }) => (
|
||||||
|
<div className={`${styles.mention_item} ${isActive ? styles.active : ''}`} onClick={() => onClick(item)}>
|
||||||
|
<div className={styles.mention_item_left}>
|
||||||
|
<Icon className={cls(styles.mention_item_icon, item.icon)} />
|
||||||
|
<span className={styles.mention_item_text}>{item.text}</span>
|
||||||
|
<span className={styles.mention_item_description}>{item.description}</span>
|
||||||
|
</div>
|
||||||
|
{item.getItems && <Icon className={cls(styles.mention_item_right, getIcon('arrowright'))} />}
|
||||||
|
</div>
|
||||||
|
);
|
|
@ -0,0 +1,89 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import styles from './mention-input.module.less';
|
||||||
|
import { MentionItem } from './mention-item';
|
||||||
|
import { MentionItem as MentionItemType, MentionPosition } from './types';
|
||||||
|
|
||||||
|
interface MentionPanelProps {
|
||||||
|
items: MentionItemType[];
|
||||||
|
activeIndex: number;
|
||||||
|
onSelectItem: (item: MentionItemType, isTriggerByClick?: boolean) => void;
|
||||||
|
position: MentionPosition;
|
||||||
|
filter: string;
|
||||||
|
visible: boolean;
|
||||||
|
level: number;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MentionPanel: React.FC<MentionPanelProps> = ({
|
||||||
|
items,
|
||||||
|
activeIndex,
|
||||||
|
onSelectItem,
|
||||||
|
position,
|
||||||
|
filter,
|
||||||
|
visible,
|
||||||
|
level,
|
||||||
|
loading = false,
|
||||||
|
}) => {
|
||||||
|
const panelRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 当活动项改变时滚动到可见区域
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (visible && panelRef.current) {
|
||||||
|
const activeItem = panelRef.current.querySelector(`.${styles.mention_item}.${styles.active}`);
|
||||||
|
if (activeItem) {
|
||||||
|
activeItem.scrollIntoView({ block: 'nearest' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [activeIndex, visible]);
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据过滤条件筛选项目
|
||||||
|
const getFilteredItems = () => {
|
||||||
|
let filteredItems = items;
|
||||||
|
|
||||||
|
if (level === 0) {
|
||||||
|
// 一级菜单根据 @ 后面的文本过滤
|
||||||
|
if (filter && filter.length > 1) {
|
||||||
|
const searchText = filter.substring(1).toLowerCase();
|
||||||
|
filteredItems = items.filter((item) => item.text.toLowerCase().includes(searchText));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 二级菜单根据 @file: 后面的文本过滤
|
||||||
|
if (filter && filter.length > 0) {
|
||||||
|
filteredItems = items.filter((item) => item.text.toLowerCase().includes(filter.toLowerCase()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredItems;
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredItems = getFilteredItems();
|
||||||
|
|
||||||
|
if (level === 0 && filteredItems.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={panelRef} className={styles.mention_panel} style={{ top: position.top, left: position.left }}>
|
||||||
|
{loading && <div className={styles.loading_bar}></div>}
|
||||||
|
{items.length > 0 ? (
|
||||||
|
<ul className={styles.mention_list}>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<MentionItem
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
isActive={index === activeIndex}
|
||||||
|
onClick={() => onSelectItem(item, true)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<div className={styles.no_results}>{loading ? '正在搜索...' : '无匹配结果'}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,84 @@
|
||||||
|
import type { LabelService } from '@opensumi/ide-core-browser';
|
||||||
|
import type { IWorkspaceService } from '@opensumi/ide-workspace';
|
||||||
|
|
||||||
|
export interface MentionItem {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
text: string;
|
||||||
|
value?: string;
|
||||||
|
description?: string;
|
||||||
|
contextId?: string;
|
||||||
|
icon?: string;
|
||||||
|
getHighestLevelItems?: () => MentionItem[];
|
||||||
|
getItems?: (searchText: string) => Promise<MentionItem[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecondLevelMenuConfig {
|
||||||
|
getDefaultItems: () => MentionItem[];
|
||||||
|
getHighestLevelItems: () => MentionItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MentionPosition {
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MentionState {
|
||||||
|
active: boolean;
|
||||||
|
startPos: number | null;
|
||||||
|
filter: string;
|
||||||
|
position: MentionPosition;
|
||||||
|
activeIndex: number;
|
||||||
|
level: number; // 0: 一级菜单, 1: 二级菜单
|
||||||
|
parentType: string | null; // 二级菜单的父类型
|
||||||
|
secondLevelFilter: string; // 二级菜单的筛选文本
|
||||||
|
inlineSearchActive: boolean; // 是否在输入框中进行二级搜索
|
||||||
|
inlineSearchStartPos: number | null; // 内联搜索的起始位置
|
||||||
|
loading: boolean; // 加载状态
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModelOption {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FooterButtonPosition {
|
||||||
|
LEFT = 'left',
|
||||||
|
RIGHT = 'right',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MentionType {
|
||||||
|
FILE = 'file',
|
||||||
|
FOLDER = 'folder',
|
||||||
|
CODE = 'code',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FooterButton {
|
||||||
|
id: string;
|
||||||
|
icon: string;
|
||||||
|
title: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
position: FooterButtonPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FooterConfig {
|
||||||
|
modelOptions?: ModelOption[];
|
||||||
|
defaultModel?: string;
|
||||||
|
buttons?: FooterButton[];
|
||||||
|
showModelSelector?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MentionInputProps {
|
||||||
|
mentionItems?: MentionItem[]; // 简化为单一菜单项配置
|
||||||
|
onSend?: (content: string, config?: { model: string; [key: string]: any }) => void;
|
||||||
|
onStop?: () => void;
|
||||||
|
placeholder?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
onSelectionChange?: (value: string) => void;
|
||||||
|
footerConfig?: FooterConfig; // 新增配置项
|
||||||
|
mentionKeyword?: string;
|
||||||
|
labelService?: LabelService;
|
||||||
|
workspaceService?: IWorkspaceService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MENTION_KEYWORD = '@';
|
|
@ -10,6 +10,7 @@ import {
|
||||||
IEditorDocumentModelService,
|
IEditorDocumentModelService,
|
||||||
} from '@opensumi/ide-editor/lib/browser/doc-model/types';
|
} from '@opensumi/ide-editor/lib/browser/doc-model/types';
|
||||||
import { EditorSelectionChangeEvent } from '@opensumi/ide-editor/lib/browser/types';
|
import { EditorSelectionChangeEvent } from '@opensumi/ide-editor/lib/browser/types';
|
||||||
|
import { FileType, IFileServiceClient } from '@opensumi/ide-file-service';
|
||||||
import { IMarkerService } from '@opensumi/ide-markers/lib/common/types';
|
import { IMarkerService } from '@opensumi/ide-markers/lib/common/types';
|
||||||
import { Range } from '@opensumi/ide-monaco';
|
import { Range } from '@opensumi/ide-monaco';
|
||||||
|
|
||||||
|
@ -26,13 +27,18 @@ export class LLMContextServiceImpl extends WithEventBus implements LLMContextSer
|
||||||
@Autowired(IMarkerService)
|
@Autowired(IMarkerService)
|
||||||
protected readonly markerService: IMarkerService;
|
protected readonly markerService: IMarkerService;
|
||||||
|
|
||||||
|
@Autowired(IFileServiceClient)
|
||||||
|
protected readonly fileService: IFileServiceClient;
|
||||||
|
|
||||||
private isAutoCollecting = false;
|
private isAutoCollecting = false;
|
||||||
|
|
||||||
private contextVersion = 0;
|
private contextVersion = 0;
|
||||||
|
|
||||||
private readonly maxAttachFilesLimit = 10;
|
private readonly maxAttachFilesLimit = 10;
|
||||||
|
private readonly maxAttachFoldersLimit = 10;
|
||||||
private readonly maxViewFilesLimit = 20;
|
private readonly maxViewFilesLimit = 20;
|
||||||
private readonly attachedFiles: FileContext[] = [];
|
private attachedFiles: FileContext[] = [];
|
||||||
|
private attachedFolders: FileContext[] = [];
|
||||||
private readonly recentlyViewFiles: FileContext[] = [];
|
private readonly recentlyViewFiles: FileContext[] = [];
|
||||||
private readonly onDidContextFilesChangeEmitter = new Emitter<{
|
private readonly onDidContextFilesChangeEmitter = new Emitter<{
|
||||||
viewed: FileContext[];
|
viewed: FileContext[];
|
||||||
|
@ -53,6 +59,18 @@ export class LLMContextServiceImpl extends WithEventBus implements LLMContextSer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private addFolderToList(folder: FileContext, list: FileContext[], maxLimit: number) {
|
||||||
|
const existingIndex = list.findIndex((f) => f.uri.toString() === folder.uri.toString());
|
||||||
|
if (existingIndex > -1) {
|
||||||
|
list.splice(existingIndex, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
list.push(folder);
|
||||||
|
if (list.length > maxLimit) {
|
||||||
|
list.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addFileToContext(uri: URI, selection?: [number, number], isManual = false): void {
|
addFileToContext(uri: URI, selection?: [number, number], isManual = false): void {
|
||||||
if (!uri) {
|
if (!uri) {
|
||||||
return;
|
return;
|
||||||
|
@ -70,12 +88,24 @@ export class LLMContextServiceImpl extends WithEventBus implements LLMContextSer
|
||||||
this.notifyContextChange();
|
this.notifyContextChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addFolderToContext(uri: URI): void {
|
||||||
|
if (!uri) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = { uri };
|
||||||
|
|
||||||
|
this.addFolderToList(file, this.attachedFolders, this.maxAttachFoldersLimit);
|
||||||
|
this.notifyContextChange();
|
||||||
|
}
|
||||||
|
|
||||||
private notifyContextChange(): void {
|
private notifyContextChange(): void {
|
||||||
this.onDidContextFilesChangeEmitter.fire(this.getAllContextFiles());
|
this.onDidContextFilesChangeEmitter.fire(this.getAllContextFiles());
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanFileContext() {
|
cleanFileContext() {
|
||||||
this.attachedFiles.length = 0;
|
this.attachedFiles = [];
|
||||||
|
this.attachedFolders = [];
|
||||||
this.notifyContextChange();
|
this.notifyContextChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,6 +113,7 @@ export class LLMContextServiceImpl extends WithEventBus implements LLMContextSer
|
||||||
return {
|
return {
|
||||||
viewed: this.recentlyViewFiles,
|
viewed: this.recentlyViewFiles,
|
||||||
attached: this.attachedFiles,
|
attached: this.attachedFiles,
|
||||||
|
attachedFolders: this.attachedFolders,
|
||||||
version: this.contextVersion++,
|
version: this.contextVersion++,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -160,16 +191,67 @@ export class LLMContextServiceImpl extends WithEventBus implements LLMContextSer
|
||||||
this.dispose();
|
this.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize(): SerializedContext {
|
async serialize(): Promise<SerializedContext> {
|
||||||
const files = this.getAllContextFiles();
|
const files = this.getAllContextFiles();
|
||||||
const workspaceRoot = URI.file(this.appConfig.workspaceDir);
|
const workspaceRoot = URI.file(this.appConfig.workspaceDir);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
recentlyViewFiles: this.serializeRecentlyViewFiles(files.viewed, workspaceRoot),
|
recentlyViewFiles: this.serializeRecentlyViewFiles(files.viewed, workspaceRoot),
|
||||||
attachedFiles: this.serializeAttachedFiles(files.attached, workspaceRoot),
|
attachedFiles: this.serializeAttachedFiles(files.attached, workspaceRoot),
|
||||||
|
attachedFolders: await this.serializeAttachedFolders(files.attachedFolders, workspaceRoot),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async serializeAttachedFolders(folders: FileContext[], workspaceRoot: URI): Promise<string[]> {
|
||||||
|
// 去重
|
||||||
|
const folderPath = Array.from(new Set(folders.map((folder) => folder.uri.toString())));
|
||||||
|
return Promise.all(
|
||||||
|
folderPath.map(async (folder) => {
|
||||||
|
const folderUri = new URI(folder);
|
||||||
|
const root = workspaceRoot.relative(folderUri)?.toString() || '/';
|
||||||
|
return `\`\`\`\n${root}\n${(await this.getPartiaFolderStructure(folderUri.codeUri.fsPath))
|
||||||
|
.map((line) => `- ${line}`)
|
||||||
|
.join('\n')}\n\`\`\`\n`;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPartiaFolderStructure(folder: string, level = 2): Promise<string[]> {
|
||||||
|
const result: string[] = [];
|
||||||
|
try {
|
||||||
|
const stat = await this.fileService.getFileStat(folder);
|
||||||
|
|
||||||
|
for (const child of stat?.children || []) {
|
||||||
|
const relativePath = new URI(folder).relative(new URI(child.uri))!.toString();
|
||||||
|
|
||||||
|
if (child.isSymbolicLink) {
|
||||||
|
// 处理软链接
|
||||||
|
const target = await this.fileService.getFileStat(child.realUri || child.uri);
|
||||||
|
if (target) {
|
||||||
|
result.push(`${relativePath} -> ${target} (symbolic link)`);
|
||||||
|
} else {
|
||||||
|
result.push(`${relativePath} (broken symbolic link)`);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child.type === FileType.Directory) {
|
||||||
|
result.push(`${relativePath}/`);
|
||||||
|
if (level > 1) {
|
||||||
|
const subDirStructure = await this.getPartiaFolderStructure(child.uri, level - 1);
|
||||||
|
result.push(...subDirStructure.map((subEntry) => `${relativePath}/${subEntry}`));
|
||||||
|
}
|
||||||
|
} else if (child.type === FileType.File) {
|
||||||
|
result.push(relativePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private serializeRecentlyViewFiles(files: FileContext[], workspaceRoot: URI): string[] {
|
private serializeRecentlyViewFiles(files: FileContext[], workspaceRoot: URI): string[] {
|
||||||
return files
|
return files
|
||||||
.map((file) => workspaceRoot.relative(file.uri)?.toString() || file.uri.parent.toString())
|
.map((file) => workspaceRoot.relative(file.uri)?.toString() || file.uri.parent.toString())
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
import { Event, URI } from '@opensumi/ide-core-common/lib/utils';
|
import { Event, URI } from '@opensumi/ide-core-common/lib/utils';
|
||||||
|
|
||||||
export interface LLMContextService {
|
export interface LLMContextService {
|
||||||
|
/**
|
||||||
|
* 开始自动收集
|
||||||
|
*/
|
||||||
startAutoCollection(): void;
|
startAutoCollection(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止自动收集
|
||||||
|
*/
|
||||||
stopAutoCollection(): void;
|
stopAutoCollection(): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -10,11 +16,19 @@ export interface LLMContextService {
|
||||||
*/
|
*/
|
||||||
addFileToContext(uri: URI, selection?: [number, number], isManual?: boolean): void;
|
addFileToContext(uri: URI, selection?: [number, number], isManual?: boolean): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加文件夹到 context 中
|
||||||
|
*/
|
||||||
|
addFolderToContext(uri: URI, isManual?: boolean): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除上下文
|
* 清除上下文
|
||||||
*/
|
*/
|
||||||
cleanFileContext(): void;
|
cleanFileContext(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上下文文件变化事件
|
||||||
|
*/
|
||||||
onDidContextFilesChangeEvent: Event<{ viewed: FileContext[]; attached: FileContext[]; version: number }>;
|
onDidContextFilesChangeEvent: Event<{ viewed: FileContext[]; attached: FileContext[]; version: number }>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,7 +38,7 @@ export interface LLMContextService {
|
||||||
removeFileFromContext(uri: URI, isManual?: boolean): void;
|
removeFileFromContext(uri: URI, isManual?: boolean): void;
|
||||||
|
|
||||||
/** 导出为可序列化格式 */
|
/** 导出为可序列化格式 */
|
||||||
serialize(): SerializedContext;
|
serialize(): Promise<SerializedContext>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileContext {
|
export interface FileContext {
|
||||||
|
@ -44,4 +58,5 @@ export interface AttachFileContext {
|
||||||
export interface SerializedContext {
|
export interface SerializedContext {
|
||||||
recentlyViewFiles: string[];
|
recentlyViewFiles: string[];
|
||||||
attachedFiles: Array<AttachFileContext>;
|
attachedFiles: Array<AttachFileContext>;
|
||||||
|
attachedFolders: string[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Autowired, Injectable } from '@opensumi/di';
|
import { Autowired, Injectable } from '@opensumi/di';
|
||||||
import { WorkbenchEditorService } from '@opensumi/ide-editor/lib/common/editor';
|
import { WorkbenchEditorService } from '@opensumi/ide-editor/lib/common/editor';
|
||||||
|
import { IWorkspaceService } from '@opensumi/ide-workspace';
|
||||||
|
|
||||||
import { SerializedContext } from '../llm-context';
|
import { SerializedContext } from '../llm-context';
|
||||||
|
|
||||||
|
@ -10,7 +11,7 @@ export interface ChatAgentPromptProvider {
|
||||||
* 提供上下文提示
|
* 提供上下文提示
|
||||||
* @param context 上下文
|
* @param context 上下文
|
||||||
*/
|
*/
|
||||||
provideContextPrompt(context: SerializedContext, userMessage: string): string;
|
provideContextPrompt(context: SerializedContext, userMessage: string): Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -18,42 +19,135 @@ export class DefaultChatAgentPromptProvider implements ChatAgentPromptProvider {
|
||||||
@Autowired(WorkbenchEditorService)
|
@Autowired(WorkbenchEditorService)
|
||||||
protected readonly workbenchEditorService: WorkbenchEditorService;
|
protected readonly workbenchEditorService: WorkbenchEditorService;
|
||||||
|
|
||||||
provideContextPrompt(context: SerializedContext, userMessage: string): string {
|
@Autowired(IWorkspaceService)
|
||||||
|
protected readonly workspaceService: IWorkspaceService;
|
||||||
|
|
||||||
|
async provideContextPrompt(context: SerializedContext, userMessage: string) {
|
||||||
|
const currentFileInfo = await this.getCurrentFileInfo();
|
||||||
|
|
||||||
|
return this.buildPromptTemplate({
|
||||||
|
recentFiles: this.buildRecentFilesSection(context.recentlyViewFiles),
|
||||||
|
attachedFiles: this.buildAttachedFilesSection(context.attachedFiles, context.recentlyViewFiles),
|
||||||
|
attachedFolders: this.buildAttachedFoldersSection(context.attachedFolders),
|
||||||
|
currentFile: currentFileInfo,
|
||||||
|
userMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getCurrentFileInfo() {
|
||||||
const editor = this.workbenchEditorService.currentEditor;
|
const editor = this.workbenchEditorService.currentEditor;
|
||||||
const currentModel = editor?.currentDocumentModel;
|
const currentModel = editor?.currentDocumentModel;
|
||||||
return `
|
|
||||||
<additional_data>
|
if (!currentModel?.uri) {
|
||||||
Below are some potentially helpful/relevant pieces of information for figuring out to respond
|
return null;
|
||||||
<recently_viewed_files>
|
}
|
||||||
${context.recentlyViewFiles.map((file, idx) => ` ${idx + 1}: ${file}`).join('\n')}
|
|
||||||
</recently_viewed_files>
|
const currentPath =
|
||||||
<attached_files>
|
(await this.workspaceService.asRelativePath(currentModel.uri))?.path || currentModel.uri.codeUri.fsPath;
|
||||||
${context.attachedFiles.map(
|
|
||||||
(file) =>
|
return {
|
||||||
`
|
path: currentPath,
|
||||||
<file_contents>
|
languageId: currentModel.languageId,
|
||||||
|
content: currentModel.getText(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildPromptTemplate({
|
||||||
|
recentFiles,
|
||||||
|
attachedFiles,
|
||||||
|
attachedFolders,
|
||||||
|
currentFile,
|
||||||
|
userMessage,
|
||||||
|
}: {
|
||||||
|
recentFiles: string;
|
||||||
|
attachedFiles: string;
|
||||||
|
attachedFolders: string;
|
||||||
|
currentFile: { path: string; languageId: string; content: string } | null;
|
||||||
|
userMessage: string;
|
||||||
|
}) {
|
||||||
|
const sections = [
|
||||||
|
'<additional_data>',
|
||||||
|
'Below are some potentially helpful/relevant pieces of information for figuring out to respond',
|
||||||
|
recentFiles,
|
||||||
|
attachedFiles,
|
||||||
|
attachedFolders,
|
||||||
|
this.buildCurrentFileSection(currentFile),
|
||||||
|
'</additional_data>',
|
||||||
|
'<user_query>',
|
||||||
|
userMessage,
|
||||||
|
'</user_query>',
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
return sections.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildRecentFilesSection(files: string[]): string {
|
||||||
|
if (!files.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<recently_viewed_files>
|
||||||
|
${files.map((file, idx) => ` ${idx + 1}: ${file}`).join('\n')}
|
||||||
|
</recently_viewed_files>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildAttachedFilesSection(
|
||||||
|
files: { path: string; content: string; lineErrors: string[] }[],
|
||||||
|
recentlyViewFiles: string[],
|
||||||
|
): string {
|
||||||
|
files = files.filter((file) => !recentlyViewFiles.includes(file.path));
|
||||||
|
if (!files.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileContents = files
|
||||||
|
.map((file) => {
|
||||||
|
const sections = [
|
||||||
|
this.buildFileContentSection(file),
|
||||||
|
file.lineErrors.length ? this.buildLineErrorsSection(file.lineErrors) : '',
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
return sections.join('\n');
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
return `<attached_files>\n${fileContents}\n</attached_files>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildFileContentSection(file: { path: string; content: string }): string {
|
||||||
|
return `<file_contents>
|
||||||
\`\`\`${file.path}
|
\`\`\`${file.path}
|
||||||
${file.content}
|
${file.content}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
</file_contents>
|
</file_contents>`;
|
||||||
<linter_errors>
|
}
|
||||||
${file.lineErrors.join('\n')}
|
|
||||||
</linter_errors>
|
private buildLineErrorsSection(errors: string[]): string {
|
||||||
`,
|
if (!errors.length) {
|
||||||
)}
|
return '';
|
||||||
</attached_files>
|
}
|
||||||
${
|
|
||||||
currentModel
|
return `<linter_errors>\n${errors.join('\n')}\n</linter_errors>`;
|
||||||
? `<current_opened_file>
|
}
|
||||||
\`\`\`${currentModel.languageId} ${currentModel.uri.toString()}
|
|
||||||
${currentModel.getText()}
|
private buildAttachedFoldersSection(folders: string[]): string {
|
||||||
|
if (!folders.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<attached_folders>\n${folders.join('\n')}</attached_folders>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildCurrentFileSection(fileInfo: { path: string; languageId: string; content: string } | null): string {
|
||||||
|
if (!fileInfo) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<current_opened_file>
|
||||||
|
\`\`\`${fileInfo.languageId} ${fileInfo.path}
|
||||||
|
${fileInfo.content}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
</current_opened_file>`
|
</current_opened_file>`;
|
||||||
: ''
|
|
||||||
}
|
|
||||||
</additional_data>
|
|
||||||
<user_query>
|
|
||||||
${userMessage}
|
|
||||||
</user_query>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,3 +52,11 @@ export const extractCodeBlocks = (content: string): string => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getToolName = (toolName: string, serverName = BUILTIN_MCP_SERVER_NAME) => `mcp_${serverName}_${toolName}`;
|
export const getToolName = (toolName: string, serverName = BUILTIN_MCP_SERVER_NAME) => `mcp_${serverName}_${toolName}`;
|
||||||
|
|
||||||
|
export const cleanAttachedTextWrapper = (text: string) => {
|
||||||
|
const rgAttachedFile = /`<attached_file>(.*)`/g;
|
||||||
|
const rgAttachedFolder = /`<attached_folder>(.*)`/g;
|
||||||
|
text = text.replace(rgAttachedFile, '$1');
|
||||||
|
text = text.replace(rgAttachedFolder, '$1');
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
|
@ -42,6 +42,7 @@ export const defaultIconfont = {
|
||||||
'cloud-download': 'cloud-download',
|
'cloud-download': 'cloud-download',
|
||||||
'cloud-server': 'cloud-server',
|
'cloud-server': 'cloud-server',
|
||||||
'code': 'code',
|
'code': 'code',
|
||||||
|
'codebraces': 'codebraces',
|
||||||
'collapse-all': 'collapse-all',
|
'collapse-all': 'collapse-all',
|
||||||
'commit': 'commit',
|
'commit': 'commit',
|
||||||
'content-search': 'content-search',
|
'content-search': 'content-search',
|
||||||
|
@ -79,6 +80,7 @@ export const defaultIconfont = {
|
||||||
'extension': 'extension',
|
'extension': 'extension',
|
||||||
'eye': 'eye',
|
'eye': 'eye',
|
||||||
'eye-close': 'eye-close',
|
'eye-close': 'eye-close',
|
||||||
|
'file': 'file',
|
||||||
'file-copy': 'file-copy',
|
'file-copy': 'file-copy',
|
||||||
'file-default': 'file-default',
|
'file-default': 'file-default',
|
||||||
'file-exclamation': 'file-exclamation',
|
'file-exclamation': 'file-exclamation',
|
||||||
|
@ -114,6 +116,7 @@ export const defaultIconfont = {
|
||||||
'loading': 'loading',
|
'loading': 'loading',
|
||||||
'magic-wand': 'magic-wand',
|
'magic-wand': 'magic-wand',
|
||||||
'max': 'max',
|
'max': 'max',
|
||||||
|
'mcp': 'mcp',
|
||||||
'menubar-dashboard-back': 'menubar-dashboard-back',
|
'menubar-dashboard-back': 'menubar-dashboard-back',
|
||||||
'menubar-edit': 'menubar-edit',
|
'menubar-edit': 'menubar-edit',
|
||||||
'menubar-file': 'menubar-file',
|
'menubar-file': 'menubar-file',
|
||||||
|
@ -162,7 +165,7 @@ export const defaultIconfont = {
|
||||||
'scm': 'scm',
|
'scm': 'scm',
|
||||||
'search': 'search',
|
'search': 'search',
|
||||||
'send': 'send',
|
'send': 'send',
|
||||||
'send-hollow': 'send-hollow',
|
'send-outlined': 'send-outlined',
|
||||||
'send-solid': 'send-solid',
|
'send-solid': 'send-solid',
|
||||||
'setting': 'setting',
|
'setting': 'setting',
|
||||||
'sever': 'sever',
|
'sever': 'sever',
|
||||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Iconfont</title>
|
<title>Iconfont</title>
|
||||||
<link rel="stylesheet" type="text/css" href="//at.alicdn.com/t/a/font_1432262_7wffopowfq.css" />
|
<link rel="stylesheet" type="text/css" href="//at.alicdn.com/t/a/font_1432262_1ef4fl5cm51.css" />
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -85,13 +85,31 @@
|
||||||
<body>
|
<body>
|
||||||
<div style="text-align: center">
|
<div style="text-align: center">
|
||||||
<h2>OpenSumi built-in icon list</h2>
|
<h2>OpenSumi built-in icon list</h2>
|
||||||
<p>OpenSumi v2.27.2</p>
|
<p>OpenSumi v3.8.1</p>
|
||||||
<p>//at.alicdn.com/t/a/font_1432262_7wffopowfq.css</p>
|
<p>//at.alicdn.com/t/a/font_1432262_1ef4fl5cm51.css</p>
|
||||||
|
|
||||||
<div>click to copy icon name</div>
|
<div>click to copy icon name</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="kaitian-icons" id="kaitian-icons">
|
<ul class="kaitian-icons" id="kaitian-icons">
|
||||||
|
<li data-icon="codebraces">
|
||||||
|
<i aria-label="图标: codebraces" class="kaitian-icon kticon-codebraces"> </i>
|
||||||
|
<div class="icon-name-wrapper">
|
||||||
|
<span class="icon-name">codebraces</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li data-icon="file">
|
||||||
|
<i aria-label="图标: file" class="kaitian-icon kticon-file"> </i>
|
||||||
|
<div class="icon-name-wrapper">
|
||||||
|
<span class="icon-name">file</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li data-icon="mcp">
|
||||||
|
<i aria-label="图标: mcp" class="kaitian-icon kticon-mcp"> </i>
|
||||||
|
<div class="icon-name-wrapper">
|
||||||
|
<span class="icon-name">mcp</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
<li data-icon="right-arrow">
|
<li data-icon="right-arrow">
|
||||||
<i aria-label="图标: right-arrow" class="kaitian-icon kticon-right-arrow"> </i>
|
<i aria-label="图标: right-arrow" class="kaitian-icon kticon-right-arrow"> </i>
|
||||||
<div class="icon-name-wrapper">
|
<div class="icon-name-wrapper">
|
||||||
|
@ -122,10 +140,10 @@
|
||||||
<span class="icon-name">afresh</span>
|
<span class="icon-name">afresh</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li data-icon="send-hollow">
|
<li data-icon="send-outlined">
|
||||||
<i aria-label="图标: send-hollow" class="kaitian-icon kticon-send-hollow"> </i>
|
<i aria-label="图标: send-outlined" class="kaitian-icon kticon-send-outlined"> </i>
|
||||||
<div class="icon-name-wrapper">
|
<div class="icon-name-wrapper">
|
||||||
<span class="icon-name">send-hollow</span>
|
<span class="icon-name">send-outlined</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li data-icon="thumbsdown">
|
<li data-icon="thumbsdown">
|
||||||
|
|
|
@ -14,17 +14,23 @@
|
||||||
/>
|
/>
|
||||||
<missing-glyph />
|
<missing-glyph />
|
||||||
|
|
||||||
<glyph glyph-name="right-arrow" unicode="" d="M332.9140625 690.56249973v-61.87499973c0-4.21875001 2.109375-8.08593723 5.41406277-10.546875L661.06250025 384 338.32812527 149.859375a13.21875027 13.21875027 0 0 1-5.41406277-10.546875v-61.87499973c0-5.2734375 6.11718751-8.4375 10.47656277-5.2734375l400.49999946 290.53124973a26.29687527 26.29687527 0 0 1 0 42.609375L343.39062527 695.83593723a6.5390625 6.5390625 0 0 1-10.47656277-5.2734375z" horiz-adv-x="1024" />
|
<glyph glyph-name="codebraces" unicode="" d="M341.333333 768C294.4 768 256 729.6 256 682.666667L256 512C256 465.066667 217.6 426.666667 170.666667 426.666667L128 426.666667 128 341.333333 170.666667 341.333333C217.6 341.333333 256 302.933333 256 256L256 85.333333C256 38.4 294.4 0 341.333333 0L426.666667 0 426.666667 85.333333 341.333333 85.333333 341.333333 298.666667C341.333333 345.6 302.933333 384 256 384 302.933333 384 341.333333 422.4 341.333333 469.333333L341.333333 682.666667 426.666667 682.666667 426.666667 768M682.666667 768C729.6 768 768 729.6 768 682.666667L768 512C768 465.066667 806.4 426.666667 853.333333 426.666667L896 426.666667 896 341.333333 853.333333 341.333333C806.4 341.333333 768 302.933333 768 256L768 85.333333C768 38.4 729.6 0 682.666667 0L597.333333 0 597.333333 85.333333 682.666667 85.333333 682.666667 298.666667C682.666667 345.6 721.066667 384 768 384 721.066667 384 682.666667 422.4 682.666667 469.333333L682.666667 682.666667 597.333333 682.666667 597.333333 768 682.666667 768Z" horiz-adv-x="1024" />
|
||||||
|
|
||||||
<glyph glyph-name="down-arrow" unicode="" d="M818.77343723 594.9375h-61.87499973c-4.14843777 0-8.08593723-2.109375-10.54687501-5.41406277l-234.14062499-322.73437501-234.07031277 322.73437501A13.21875027 13.21875027 0 0 1 267.5234375 594.9375h-61.87499973a6.60937473 6.60937473 0 0 1-5.2734375-10.47656277L490.90624999 183.96093777c10.546875-14.5546875 32.13281223-14.5546875 42.60937501 0l290.53124973 400.49999946A6.5390625 6.5390625 0 0 1 818.77343723 594.9375z" horiz-adv-x="1024" />
|
<glyph glyph-name="file" unicode="" d="M288 768C235.264 768 192 724.736 192 672v-576c0-52.736 43.264-96 96-96h448c52.736 0 96 43.264 96 96V557.248L621.248 768z m0-64H576v-192h192v-416c0-17.984-14.016-32-32-32h-448a31.616 31.616 0 0 0-32 32v576c0 17.984 14.016 32 32 32z m352-45.248L722.752 576H640z" horiz-adv-x="1024" />
|
||||||
|
|
||||||
|
<glyph glyph-name="mcp" unicode="" d="M469.1968 779.63264c59.98592 59.98592 157.2352 59.98592 217.22112 0 59.38688-59.38176 59.9808-155.29472 1.78176-215.41376l-1.78176-1.80736L423.936 299.9296a30.72 30.72 0 0 0-44.34432 42.51136l0.896 0.93696L642.9696 605.8496c35.9936 35.9936 35.9936 94.34112 0 130.33472-35.5328 35.53792-92.88192 35.98336-128.9728 1.3312l-1.3568-1.3312-347.55584-347.55584a30.72 30.72 0 0 0-44.34432 42.50624l0.896 0.93696L469.1968 779.63264zM642.9696 605.85472c59.392 59.38176 155.29984 59.97568 215.43424 1.7664l1.81248-1.78176 1.792-1.792c59.38688-59.392 59.9808-155.29984 1.78176-215.41376l-1.78176-1.81248-314.33728-314.33728a10.24 10.24 0 0 1-0.59392-13.824l0.59904-0.65024 64.54272-64.54784a30.72 30.72 0 0 0-42.51136-44.34432l-0.93696 0.896-64.54272 64.54784c-27.56096 27.56096-27.98592 71.9872-1.26976 100.06528l1.26976 1.3056 314.33216 314.33216c35.54304 35.54304 35.9936 92.88704 1.32096 128.98816l-1.3312 1.36192-1.792 1.792c-35.54816 35.54304-92.89216 35.98848-128.98304 1.33632l-1.3568-1.3312-258.85696-258.86208a30.72 30.72 0 0 0-44.34432 42.51136l0.90112 0.93696L642.9696 605.8496zM556.0832 692.74624a30.72 30.72 0 0 0 44.34432-42.51136l-0.896-0.93696L342.4768 392.25344c-35.9936-35.9936-35.9936-94.34624 0-130.33472 35.53792-35.53792 92.88704-35.98336 128.9728-1.3312l1.36192 1.3312L729.856 518.9632a30.72 30.72 0 0 0 44.34432-42.50624l-0.896-0.93696-257.0496-257.0496c-59.98592-59.9808-157.2352-59.9808-217.22624 0-59.38176 59.392-59.97568 155.29984-1.77664 215.41376l1.78176 1.81248 257.04448 257.0496z" horiz-adv-x="1024" />
|
||||||
|
|
||||||
|
<glyph glyph-name="right-arrow" unicode="" d="M332.914063 690.5625v-61.875c0-4.21875 2.109375-8.085937 5.414062-10.546875L661.0625 384 338.328125 149.859375a13.21875 13.21875 0 0 1-5.414063-10.546875v-61.875c0-5.273438 6.117188-8.4375 10.476563-5.273437l400.5 290.53125a26.296875 26.296875 0 0 1 0 42.609375L343.390625 695.835937a6.539063 6.539063 0 0 1-10.476563-5.273437z" horiz-adv-x="1024" />
|
||||||
|
|
||||||
|
<glyph glyph-name="down-arrow" unicode="" d="M818.773437 594.9375h-61.874999c-4.148438 0-8.085937-2.109375-10.546876-5.414063l-234.140624-322.734375-234.070313 322.734375A13.21875 13.21875 0 0 1 267.523438 594.9375h-61.875a6.609375 6.609375 0 0 1-5.273438-10.476563L490.90625 183.960938c10.546875-14.554688 32.132812-14.554688 42.609375 0l290.53125 400.499999A6.539063 6.539063 0 0 1 818.773437 594.9375z" horiz-adv-x="1024" />
|
||||||
|
|
||||||
<glyph glyph-name="run" unicode="" d="M244.565333 810.666667A74.666667 74.666667 0 0 1 170.666667 735.232v-702.464c0-43.946667 35.328-75.434667 73.898666-75.434667a71.168 71.168 0 0 1 36.693334 10.24l297.898666 175.616L878.933333 320c51.2 30.208 51.2 98.048 0 128.256l-299.776 176.64-297.813333 175.616A72.533333 72.533333 0 0 1 244.650667 810.666667z" horiz-adv-x="1024" />
|
<glyph glyph-name="run" unicode="" d="M244.565333 810.666667A74.666667 74.666667 0 0 1 170.666667 735.232v-702.464c0-43.946667 35.328-75.434667 73.898666-75.434667a71.168 71.168 0 0 1 36.693334 10.24l297.898666 175.616L878.933333 320c51.2 30.208 51.2 98.048 0 128.256l-299.776 176.64-297.813333 175.616A72.533333 72.533333 0 0 1 244.650667 810.666667z" horiz-adv-x="1024" />
|
||||||
|
|
||||||
<glyph glyph-name="discard" unicode="" d="M414.144 328.832a42.688 42.688 0 1 0-60.288-60.352L140.48 481.92l-3.072 3.456-0.32 0.384-0.704 0.896a49.536 49.536 0 0 0-5.312 9.6l-0.96 2.56-0.576 1.92a65.6 65.6 0 0 0-0.64 2.752l-0.448 2.24A43.2 43.2 0 0 0 128 512l0.192-3.84A66.24 66.24 0 0 0 128 511.232v1.664l0.192 2.944 0.256 2.56 0.384 2.24 0.64 2.752 0.64 1.984 0.896 2.56a34.88 34.88 0 0 0 2.304 4.8l0.896 1.536a40.896 40.896 0 0 0 6.272 7.936l-3.392-3.84a42.112 42.112 0 0 0 3.392 3.84L353.92 755.456a42.688 42.688 0 1 0 60.288-60.352L273.664 554.688h408.96a213.312 213.312 0 0 0 213.12-204.096l0.256-9.28v-298.624a42.688 42.688 0 0 0-85.312 0V341.312a128 128 0 0 1-128 128H273.664l140.48-140.48z" horiz-adv-x="1024" />
|
<glyph glyph-name="discard" unicode="" d="M414.144 328.832a42.688 42.688 0 1 0-60.288-60.352L140.48 481.92l-3.072 3.456-0.32 0.384-0.704 0.896a49.536 49.536 0 0 0-5.312 9.6l-0.96 2.56-0.576 1.92a65.6 65.6 0 0 0-0.64 2.752l-0.448 2.24A43.2 43.2 0 0 0 128 512l0.192-3.84A66.24 66.24 0 0 0 128 511.232v1.664l0.192 2.944 0.256 2.56 0.384 2.24 0.64 2.752 0.64 1.984 0.896 2.56a34.88 34.88 0 0 0 2.304 4.8l0.896 1.536a40.896 40.896 0 0 0 6.272 7.936l-3.392-3.84a42.112 42.112 0 0 0 3.392 3.84L353.92 755.456a42.688 42.688 0 1 0 60.288-60.352L273.664 554.688h408.96a213.312 213.312 0 0 0 213.12-204.096l0.256-9.28v-298.624a42.688 42.688 0 0 0-85.312 0V341.312a128 128 0 0 1-128 128H273.664l140.48-140.48z" horiz-adv-x="1024" />
|
||||||
|
|
||||||
<glyph glyph-name="afresh" unicode="" d="M758.208 56.960000000000036a399.296 399.296 0 0 1 153.792 315.136 399.616 399.616 0 0 1-399.36 399.936 399.936 399.936 0 0 1-253.056-710.144 8.128 8.128 0 0 1 11.392 1.28l39.424 50.56a8 8 0 0 1-1.216 11.072A318.656 318.656 0 0 0 192 372.096a318.656 318.656 0 0 0 93.696 226.176A318.656 318.656 0 0 0 512 692.032a318.656 318.656 0 0 0 226.176-93.696 318.656 318.656 0 0 0 93.696-226.24 318.656 318.656 0 0 0-123.008-252.16l-40.64 52.032a8 8 0 0 1-14.08-2.944l-39.68-162.24a8 8 0 0 1 7.68-9.856l167.04-0.832c6.72 0 10.496 7.68 6.336 12.928l-37.312 47.872z" horiz-adv-x="1024" />
|
<glyph glyph-name="afresh" unicode="" d="M758.208 56.96a399.296 399.296 0 0 1 153.792 315.136 399.616 399.616 0 0 1-399.36 399.936 399.936 399.936 0 0 1-253.056-710.144 8.128 8.128 0 0 1 11.392 1.28l39.424 50.56a8 8 0 0 1-1.216 11.072A318.656 318.656 0 0 0 192 372.096a318.656 318.656 0 0 0 93.696 226.176A318.656 318.656 0 0 0 512 692.032a318.656 318.656 0 0 0 226.176-93.696 318.656 318.656 0 0 0 93.696-226.24 318.656 318.656 0 0 0-123.008-252.16l-40.64 52.032a8 8 0 0 1-14.08-2.944l-39.68-162.24a8 8 0 0 1 7.68-9.856l167.04-0.832c6.72 0 10.496 7.68 6.336 12.928l-37.312 47.872z" horiz-adv-x="1024" />
|
||||||
|
|
||||||
<glyph glyph-name="send-hollow" unicode="" d="M896.170667 727.808c26.453333-23.637333 37.546667-59.904 29.013333-94.122667L790.869333 88.57600000000002a95.061333 95.061333 0 0 0-139.434666-59.989333L498.005333 116.22400000000005l-31.232-31.573333c-19.712-19.882667-46.933333-29.866667-74.24-27.904l-11.776 1.706666a87.125333 87.125333 0 0 0-20.821333 7.253334 194.389333 194.389333 0 0 0-24.917333 19.626666c-11.093333 10.154667-19.626667 20.48-25.088 32-8.533333 17.92-21.930667 49.749333-38.656 91.562667-8.533333 21.333333-17.322667 43.52-24.917334 63.146667l-3.328 8.448-93.781333 46.08a94.549333 94.549333 0 0 0-52.992 78.762666l-0.170667 11.434667c1.706667 38.4 25.429333 71.168 61.269334 84.906667l641.877333 244.053333c33.28 12.714667 70.485333 5.802667 96.938667-17.92z m-74.24-41.813333l-641.706667-244.138667a30.634667 30.634667 0 0 1-20.224-27.989333 30.72 30.72 0 0 1 17.578667-29.866667l99.84-49.152c5.12-2.56 9.472-5.632 13.482666-9.386667l7.936-11.861333c23.04-59.904 48.213333-122.88 63.402667-156.757333l-1.024-3.754667 8.192 34.474667 22.954667 93.781333 1.792 7.765333 1.194666 6.314667c2.133333 11.946667 3.84 21.333333 4.778667 25.429333l0.512 1.962667-1.109333-2.816-0.597334-1.194667 0.426667 1.109334c0.256 1.28 0.512 1.877333 1.194667 3.925333l0.682666 1.621333 1.28 2.986667a40.362667 40.362667 0 0 0 8.533334 11.946667l4.181333 3.925333a63495.850667 63495.850667 0 0 0 205.226667 183.466667l9.813333 8.448 3.584 2.816c-0.085333 1.024-0.085333 1.024 18.773333 6.485333 26.112-8.96 26.112-8.96 31.232-40.96-1.194667-3.584-1.194667-3.584-3.242666-7.509333-2.304-3.413333-2.304-3.413333-3.242667-4.522667a95.744 95.744 0 0 0-4.949333-4.778667l-2.389334-2.218666c-19.2-17.834667-77.226667-69.973333-197.973333-178.090667l-9.557333-8.533333-2.901334-12.373334-3.413333-13.312-0.853333-5.632c-2.816-16.981333-6.656-37.973333-10.24-55.722666l-1.877334-8.533334a524.8 524.8 0 0 0-19.626666-71.253333l34.986666 35.242667a53.077333 53.077333 0 0 0 63.829334 8.704l160.768-91.904a30.976 30.976 0 0 1 27.477333-1.621334 31.061333 31.061333 0 0 1 18.090667 21.418667l134.314666 545.28a31.061333 31.061333 0 0 1-41.130666 36.693333z" horiz-adv-x="1024" />
|
<glyph glyph-name="send-outlined" unicode="" d="M896.170667 727.808c26.453333-23.637333 37.546667-59.904 29.013333-94.122667L790.869333 88.576a95.061333 95.061333 0 0 0-139.434666-59.989333L498.005333 116.224l-31.232-31.573333c-19.712-19.882667-46.933333-29.866667-74.24-27.904l-11.776 1.706666a87.125333 87.125333 0 0 0-20.821333 7.253334 194.389333 194.389333 0 0 0-24.917333 19.626666c-11.093333 10.154667-19.626667 20.48-25.088 32-8.533333 17.92-21.930667 49.749333-38.656 91.562667-8.533333 21.333333-17.322667 43.52-24.917334 63.146667l-3.328 8.448-93.781333 46.08a94.549333 94.549333 0 0 0-52.992 78.762666l-0.170667 11.434667c1.706667 38.4 25.429333 71.168 61.269334 84.906667l641.877333 244.053333c33.28 12.714667 70.485333 5.802667 96.938667-17.92z m-74.24-41.813333l-641.706667-244.138667a30.634667 30.634667 0 0 1-20.224-27.989333 30.72 30.72 0 0 1 17.578667-29.866667l99.84-49.152c5.12-2.56 9.472-5.632 13.482666-9.386667l7.936-11.861333c23.04-59.904 48.213333-122.88 63.402667-156.757333l-1.024-3.754667 8.192 34.474667 22.954667 93.781333 1.792 7.765333 1.194666 6.314667c2.133333 11.946667 3.84 21.333333 4.778667 25.429333l0.512 1.962667-1.109333-2.816-0.597334-1.194667 0.426667 1.109334c0.256 1.28 0.512 1.877333 1.194667 3.925333l0.682666 1.621333 1.28 2.986667a40.362667 40.362667 0 0 0 8.533334 11.946667l4.181333 3.925333a63495.850667 63495.850667 0 0 0 205.226667 183.466667l9.813333 8.448 3.584 2.816c-0.085333 1.024-0.085333 1.024 18.773333 6.485333 26.112-8.96 26.112-8.96 31.232-40.96-1.194667-3.584-1.194667-3.584-3.242666-7.509333-2.304-3.413333-2.304-3.413333-3.242667-4.522667a95.744 95.744 0 0 0-4.949333-4.778667l-2.389334-2.218666c-19.2-17.834667-77.226667-69.973333-197.973333-178.090667l-9.557333-8.533333-2.901334-12.373334-3.413333-13.312-0.853333-5.632c-2.816-16.981333-6.656-37.973333-10.24-55.722666l-1.877334-8.533334a524.8 524.8 0 0 0-19.626666-71.253333l34.986666 35.242667a53.077333 53.077333 0 0 0 63.829334 8.704l160.768-91.904a30.976 30.976 0 0 1 27.477333-1.621334 31.061333 31.061333 0 0 1 18.090667 21.418667l134.314666 545.28a31.061333 31.061333 0 0 1-41.130666 36.693333z" horiz-adv-x="1024" />
|
||||||
|
|
||||||
<glyph glyph-name="thumbsdown" unicode="" d="M621.696 280c16.64-74.24 28.16-127.936 34.816-161.152 16.64-83.776-26.56-161.536-112.192-161.536-77.248 0-116.032 38.4-138.88 115.136l-0.64 2.24c-13.696 62.08-34.688 110.144-62.464 144.576a158.272 158.272 0 0 1-119.744 58.944l-21.888 0.448a96.448 96.448 0 0 0-94.08 96.832V693.312c0 64.832 52.16 117.376 116.48 117.376H635.52c84.736 0 160.384-53.568 189.12-133.952l85.696-239.552a117.632 117.632 0 0 0-70.016-150.208 115.584 115.584 0 0 0-39.488-6.976h-179.2z m-77.44-258.688c39.232 0 59.52 36.48 49.92 84.928-7.616 38.144-22.016 104.448-43.264 198.656a31.936 31.936 0 0 0 30.912 39.104h218.688a53.504 53.504 0 0 1 49.664 71.424l-85.568 239.616a137.408 137.408 0 0 1-129.216 91.648H223.488a53.12 53.12 0 0 1-52.8-53.376v-317.824c0-17.856 14.08-32.448 31.808-32.832l21.888-0.448a221.12 221.12 0 0 0 167.36-82.56c34.368-42.624 59.136-99.328 74.88-169.856 15.488-51.456 32.896-68.48 77.632-68.48z" horiz-adv-x="1024" />
|
<glyph glyph-name="thumbsdown" unicode="" d="M621.696 280c16.64-74.24 28.16-127.936 34.816-161.152 16.64-83.776-26.56-161.536-112.192-161.536-77.248 0-116.032 38.4-138.88 115.136l-0.64 2.24c-13.696 62.08-34.688 110.144-62.464 144.576a158.272 158.272 0 0 1-119.744 58.944l-21.888 0.448a96.448 96.448 0 0 0-94.08 96.832V693.312c0 64.832 52.16 117.376 116.48 117.376H635.52c84.736 0 160.384-53.568 189.12-133.952l85.696-239.552a117.632 117.632 0 0 0-70.016-150.208 115.584 115.584 0 0 0-39.488-6.976h-179.2z m-77.44-258.688c39.232 0 59.52 36.48 49.92 84.928-7.616 38.144-22.016 104.448-43.264 198.656a31.936 31.936 0 0 0 30.912 39.104h218.688a53.504 53.504 0 0 1 49.664 71.424l-85.568 239.616a137.408 137.408 0 0 1-129.216 91.648H223.488a53.12 53.12 0 0 1-52.8-53.376v-317.824c0-17.856 14.08-32.448 31.808-32.832l21.888-0.448a221.12 221.12 0 0 0 167.36-82.56c34.368-42.624 59.136-99.328 74.88-169.856 15.488-51.456 32.896-68.48 77.632-68.48z" horiz-adv-x="1024" />
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 155 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -59,12 +59,9 @@ export class MarkdownReactParser extends marked.Renderer {
|
||||||
|
|
||||||
listItemChildren.push(this.parse(item.tokens));
|
listItemChildren.push(this.parse(item.tokens));
|
||||||
|
|
||||||
return React.cloneElement(
|
return React.cloneElement(this.renderer.listItem(listItemChildren) as React.ReactElement, {
|
||||||
this.renderer.listItem(listItemChildren) as React.ReactElement,
|
|
||||||
{
|
|
||||||
key: `list-item-${itemIndex}`,
|
key: `list-item-${itemIndex}`,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.renderer.list(children, token.ordered);
|
return this.renderer.list(children, token.ordered);
|
||||||
|
@ -86,12 +83,9 @@ export class MarkdownReactParser extends marked.Renderer {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const headerRow = React.cloneElement(
|
const headerRow = React.cloneElement(this.renderer.tableRow(headerCells) as React.ReactElement, {
|
||||||
this.renderer.tableRow(headerCells) as React.ReactElement,
|
|
||||||
{
|
|
||||||
key: 'header-row',
|
key: 'header-row',
|
||||||
},
|
});
|
||||||
);
|
|
||||||
const header = this.renderer.tableHeader(headerRow);
|
const header = this.renderer.tableHeader(headerRow);
|
||||||
|
|
||||||
const bodyChilren = tableToken.rows.map((row, rowIndex) => {
|
const bodyChilren = tableToken.rows.map((row, rowIndex) => {
|
||||||
|
@ -105,12 +99,9 @@ export class MarkdownReactParser extends marked.Renderer {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return React.cloneElement(
|
return React.cloneElement(this.renderer.tableRow(rowChildren) as React.ReactElement, {
|
||||||
this.renderer.tableRow(rowChildren) as React.ReactElement,
|
|
||||||
{
|
|
||||||
key: `body-row-${rowIndex}`,
|
key: `body-row-${rowIndex}`,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const body = this.renderer.tableBody(bodyChilren);
|
const body = this.renderer.tableBody(bodyChilren);
|
||||||
|
@ -194,7 +185,5 @@ export class MarkdownReactParser extends marked.Renderer {
|
||||||
function htmlUnescape(htmlStr) {
|
function htmlUnescape(htmlStr) {
|
||||||
return htmlStr
|
return htmlStr
|
||||||
.replace(/&#(\d+);/g, (match, dec) => String.fromCharCode(dec))
|
.replace(/&#(\d+);/g, (match, dec) => String.fromCharCode(dec))
|
||||||
.replace(/&#x([0-9A-Fa-f]+);/g, (match, hex) =>
|
.replace(/&#x([0-9A-Fa-f]+);/g, (match, hex) => String.fromCharCode(parseInt(hex, 16)));
|
||||||
String.fromCharCode(parseInt(hex, 16)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,6 @@ export class AINativeConfigService implements IAINativeConfig {
|
||||||
private internalCapabilities = DEFAULT_CAPABILITIES;
|
private internalCapabilities = DEFAULT_CAPABILITIES;
|
||||||
private internalInlineChat = DEFAULT_INLINE_CHAT_CONFIG;
|
private internalInlineChat = DEFAULT_INLINE_CHAT_CONFIG;
|
||||||
private internalCodeEdits = DEFAULT_CODE_EDITS_CONFIG;
|
private internalCodeEdits = DEFAULT_CODE_EDITS_CONFIG;
|
||||||
|
|
||||||
public get capabilities(): Required<IAINativeCapabilities> {
|
public get capabilities(): Required<IAINativeCapabilities> {
|
||||||
if (!this.aiModuleLoaded) {
|
if (!this.aiModuleLoaded) {
|
||||||
return DISABLED_ALL_CAPABILITIES;
|
return DISABLED_ALL_CAPABILITIES;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export const IDE_ICONFONT_CN_CSS = '//at.alicdn.com/t/a/font_1432262_7wffopowfq.css';
|
export const IDE_ICONFONT_CN_CSS = '//at.alicdn.com/t/a/font_1432262_1ef4fl5cm51.css';
|
||||||
|
|
||||||
export const IDE_ICONFONT_CN_JS = IDE_ICONFONT_CN_CSS.replace(/\.css$/, '.js');
|
export const IDE_ICONFONT_CN_JS = IDE_ICONFONT_CN_CSS.replace(/\.css$/, '.js');
|
||||||
|
|
||||||
|
|
|
@ -218,6 +218,7 @@ html {
|
||||||
.kt-overlay .kt-modal-body {
|
.kt-overlay .kt-modal-body {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 0 none !important;
|
border: 0 none !important;
|
||||||
|
padding: 24px;
|
||||||
box-shadow: 0px 9px 28px 8px var(--design-boxShadow-primary), 0px 3px 6px -4px var(--design-boxShadow-secondary),
|
box-shadow: 0px 9px 28px 8px var(--design-boxShadow-primary), 0px 3px 6px -4px var(--design-boxShadow-secondary),
|
||||||
0px 6px 16px 0px var(--design-boxShadow-tertiary) !important;
|
0px 6px 16px 0px var(--design-boxShadow-tertiary) !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ export namespace IFileSearchService {
|
||||||
noIgnoreParent?: boolean; // 是否忽略祖先目录的 gitIgnore
|
noIgnoreParent?: boolean; // 是否忽略祖先目录的 gitIgnore
|
||||||
includePatterns?: string[];
|
includePatterns?: string[];
|
||||||
excludePatterns?: string[];
|
excludePatterns?: string[];
|
||||||
|
onlyFolders?: boolean;
|
||||||
}
|
}
|
||||||
export interface RootOptions {
|
export interface RootOptions {
|
||||||
[rootUri: string]: BaseOptions;
|
[rootUri: string]: BaseOptions;
|
||||||
|
|
|
@ -40,6 +40,7 @@ export class FileSearchService implements IFileSearchService {
|
||||||
fuzzyMatch: true,
|
fuzzyMatch: true,
|
||||||
limit: Number.MAX_SAFE_INTEGER,
|
limit: Number.MAX_SAFE_INTEGER,
|
||||||
useGitIgnore: true,
|
useGitIgnore: true,
|
||||||
|
onlyFolders: false,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -92,6 +93,19 @@ export class FileSearchService implements IFileSearchService {
|
||||||
rootOptions,
|
rootOptions,
|
||||||
(candidate) => {
|
(candidate) => {
|
||||||
const fileUri = path.join(cwd, candidate);
|
const fileUri = path.join(cwd, candidate);
|
||||||
|
|
||||||
|
if (opts.onlyFolders) {
|
||||||
|
try {
|
||||||
|
const fs = require('fs');
|
||||||
|
const stat = fs.statSync(fileUri);
|
||||||
|
if (!stat.isDirectory()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (exactMatches.has(fileUri) || fuzzyMatches.has(fileUri)) {
|
if (exactMatches.has(fileUri) || fuzzyMatches.has(fileUri)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -117,7 +131,7 @@ export class FileSearchService implements IFileSearchService {
|
||||||
);
|
);
|
||||||
const sortedExactMatches = Array.from(exactMatches).sort((a, b) => {
|
const sortedExactMatches = Array.from(exactMatches).sort((a, b) => {
|
||||||
const depthA = Path.pathDepth(a);
|
const depthA = Path.pathDepth(a);
|
||||||
const depthB = Path.pathDepth(a);
|
const depthB = Path.pathDepth(b);
|
||||||
if (depthA === depthB) {
|
if (depthA === depthB) {
|
||||||
const dirA = dirname(a);
|
const dirA = dirname(a);
|
||||||
const dirB = dirname(b);
|
const dirB = dirname(b);
|
||||||
|
@ -139,6 +153,12 @@ export class FileSearchService implements IFileSearchService {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const args = this.getSearchArgs(options);
|
const args = this.getSearchArgs(options);
|
||||||
|
|
||||||
|
if (options.onlyFolders) {
|
||||||
|
args.push('--type-list', 'd:dir');
|
||||||
|
args.push('--type', 'd');
|
||||||
|
}
|
||||||
|
|
||||||
const process = this.processFactory.create({ command: replaceAsarInPath(rgPath), args, options: { cwd } });
|
const process = this.processFactory.create({ command: replaceAsarInPath(rgPath), args, options: { cwd } });
|
||||||
process.onError(reject);
|
process.onError(reject);
|
||||||
process.outputStream.on('close', resolve);
|
process.outputStream.on('close', resolve);
|
||||||
|
@ -162,6 +182,12 @@ export class FileSearchService implements IFileSearchService {
|
||||||
|
|
||||||
private getSearchArgs(options: IFileSearchService.BaseOptions): string[] {
|
private getSearchArgs(options: IFileSearchService.BaseOptions): string[] {
|
||||||
const args = ['--files', '--hidden', '--case-sensitive', '--no-require-git'];
|
const args = ['--files', '--hidden', '--case-sensitive', '--no-require-git'];
|
||||||
|
|
||||||
|
if (options.onlyFolders) {
|
||||||
|
args.push('--type-list', 'd:dir');
|
||||||
|
args.push('--type', 'd');
|
||||||
|
}
|
||||||
|
|
||||||
if (options.includePatterns) {
|
if (options.includePatterns) {
|
||||||
for (const includePattern of options.includePatterns) {
|
for (const includePattern of options.includePatterns) {
|
||||||
if (includePattern) {
|
if (includePattern) {
|
||||||
|
|
|
@ -1016,9 +1016,10 @@ export class FileTreeContribution
|
||||||
const handler = this.mainLayoutService.getTabbarHandler(EXPLORER_CONTAINER_ID);
|
const handler = this.mainLayoutService.getTabbarHandler(EXPLORER_CONTAINER_ID);
|
||||||
if (handler && !handler.isVisible) {
|
if (handler && !handler.isVisible) {
|
||||||
handler.activate();
|
handler.activate();
|
||||||
}
|
setTimeout(() => {
|
||||||
if (handler && handler.isCollapsed(RESOURCE_VIEW_ID)) {
|
// FIXME: 目前通过 handler.isCollapsed 方法获取到的这段状态不准确
|
||||||
handler?.setCollapsed(RESOURCE_VIEW_ID, false);
|
handler.setCollapsed(RESOURCE_VIEW_ID, false);
|
||||||
|
}, 200);
|
||||||
}
|
}
|
||||||
if (!uri && this.workbenchEditorService.currentEditor?.currentUri) {
|
if (!uri && this.workbenchEditorService.currentEditor?.currentUri) {
|
||||||
uri = this.workbenchEditorService.currentEditor.currentUri;
|
uri = this.workbenchEditorService.currentEditor.currentUri;
|
||||||
|
|
|
@ -1449,7 +1449,7 @@ export const localizationBundle = {
|
||||||
|
|
||||||
// #region AI Native
|
// #region AI Native
|
||||||
'aiNative.chat.ai.assistant.name': 'AI Assistant',
|
'aiNative.chat.ai.assistant.name': 'AI Assistant',
|
||||||
'aiNative.chat.input.placeholder.default': 'Ask Copilot or type / for commands',
|
'aiNative.chat.input.placeholder.default': 'Ask anything, @ to mention',
|
||||||
'aiNative.chat.stop.immediately': 'I don’t think about it anymore. If you need anything, you can ask me anytime.',
|
'aiNative.chat.stop.immediately': 'I don’t think about it anymore. If you need anything, you can ask me anytime.',
|
||||||
'aiNative.chat.error.response':
|
'aiNative.chat.error.response':
|
||||||
'There are too many people interacting with me at the moment. Please try again later. Thank you for your understanding and support.',
|
'There are too many people interacting with me at the moment. Please try again later. Thank you for your understanding and support.',
|
||||||
|
@ -1459,6 +1459,8 @@ export const localizationBundle = {
|
||||||
'aiNative.chat.expand.unfullscreen': 'unfullscreen',
|
'aiNative.chat.expand.unfullscreen': 'unfullscreen',
|
||||||
'aiNative.chat.expand.fullescreen': 'fullescreen',
|
'aiNative.chat.expand.fullescreen': 'fullescreen',
|
||||||
'aiNative.chat.enter.send': 'Send (Enter)',
|
'aiNative.chat.enter.send': 'Send (Enter)',
|
||||||
|
'aiNative.chat.defaultContextFile': 'Current File',
|
||||||
|
'aiNative.chat.defaultContextFolder': 'Current Folder',
|
||||||
'aiNative.chat.thinking': 'Deep Think',
|
'aiNative.chat.thinking': 'Deep Think',
|
||||||
|
|
||||||
'aiNative.inline.chat.operate.chat.title': 'Chat({0})',
|
'aiNative.inline.chat.operate.chat.title': 'Chat({0})',
|
||||||
|
|
|
@ -1218,7 +1218,7 @@ export const localizationBundle = {
|
||||||
|
|
||||||
// #region AI Native
|
// #region AI Native
|
||||||
'aiNative.chat.ai.assistant.name': 'AI 研发助手',
|
'aiNative.chat.ai.assistant.name': 'AI 研发助手',
|
||||||
'aiNative.chat.input.placeholder.default': '可以问我任何问题,或键入主题 "/" ',
|
'aiNative.chat.input.placeholder.default': '可以问我任何问题,输入 @ 可引用内容',
|
||||||
'aiNative.chat.stop.immediately': '我先不想了,有需要可以随时问我',
|
'aiNative.chat.stop.immediately': '我先不想了,有需要可以随时问我',
|
||||||
'aiNative.chat.error.response': '当前与我互动的人太多,请稍后再试,感谢您的理解与支持',
|
'aiNative.chat.error.response': '当前与我互动的人太多,请稍后再试,感谢您的理解与支持',
|
||||||
'aiNative.chat.code.insert': '插入代码',
|
'aiNative.chat.code.insert': '插入代码',
|
||||||
|
@ -1227,6 +1227,8 @@ export const localizationBundle = {
|
||||||
'aiNative.chat.expand.unfullscreen': '收起',
|
'aiNative.chat.expand.unfullscreen': '收起',
|
||||||
'aiNative.chat.expand.fullescreen': '展开全屏',
|
'aiNative.chat.expand.fullescreen': '展开全屏',
|
||||||
'aiNative.chat.enter.send': 'Enter 发送',
|
'aiNative.chat.enter.send': 'Enter 发送',
|
||||||
|
'aiNative.chat.defaultContextFile': '当前文件',
|
||||||
|
'aiNative.chat.defaultContextFolder': '当前文件夹',
|
||||||
'aiNative.chat.thinking': '深度思考',
|
'aiNative.chat.thinking': '深度思考',
|
||||||
|
|
||||||
'aiNative.inline.chat.operate.chat.title': 'Chat({0})',
|
'aiNative.inline.chat.operate.chat.title': 'Chat({0})',
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { Color, RGBA } from '../color';
|
||||||
|
import { registerColor, transparent } from '../utils';
|
||||||
|
|
||||||
|
import { badgeBackground, badgeForeground } from './badge';
|
||||||
|
import { contrastBorder, foreground } from './base';
|
||||||
|
import { editorBackground, editorWidgetBackground } from './editor';
|
||||||
|
|
||||||
|
export const chatRequestBorder = registerColor(
|
||||||
|
'chat.requestBorder',
|
||||||
|
{
|
||||||
|
dark: new Color(new RGBA(255, 255, 255, 0.1)),
|
||||||
|
light: new Color(new RGBA(0, 0, 0, 0.1)),
|
||||||
|
hcDark: contrastBorder,
|
||||||
|
hcLight: contrastBorder,
|
||||||
|
},
|
||||||
|
'The border color of a chat request.',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const chatRequestBackground = registerColor(
|
||||||
|
'chat.requestBackground',
|
||||||
|
{
|
||||||
|
dark: transparent(editorBackground, 0.62),
|
||||||
|
light: transparent(editorBackground, 0.62),
|
||||||
|
hcDark: editorWidgetBackground,
|
||||||
|
hcLight: null,
|
||||||
|
},
|
||||||
|
'The background color of a chat request.',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const chatSlashCommandBackground = registerColor(
|
||||||
|
'chat.slashCommandBackground',
|
||||||
|
{ dark: '#34414b8f', light: '#d2ecff99', hcDark: Color.white, hcLight: badgeBackground },
|
||||||
|
'The background color of a chat slash command.',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const chatSlashCommandForeground = registerColor(
|
||||||
|
'chat.slashCommandForeground',
|
||||||
|
{ dark: '#40A6FF', light: '#306CA2', hcDark: Color.black, hcLight: badgeForeground },
|
||||||
|
'The foreground color of a chat slash command.',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const chatAvatarBackground = registerColor(
|
||||||
|
'chat.avatarBackground',
|
||||||
|
{ dark: '#1f1f1f', light: '#f2f2f2', hcDark: Color.black, hcLight: Color.white },
|
||||||
|
'The background color of a chat avatar.',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const chatAvatarForeground = registerColor(
|
||||||
|
'chat.avatarForeground',
|
||||||
|
{ dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground },
|
||||||
|
'The foreground color of a chat avatar.',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const chatEditedFileForeground = registerColor(
|
||||||
|
'chat.editedFileForeground',
|
||||||
|
{
|
||||||
|
light: '#895503',
|
||||||
|
dark: '#E2C08D',
|
||||||
|
hcDark: '#E2C08D',
|
||||||
|
hcLight: '#895503',
|
||||||
|
},
|
||||||
|
'The foreground color of a chat edited file in the edited file list.',
|
||||||
|
);
|
|
@ -35,5 +35,5 @@ export * from './minimap';
|
||||||
export * from './testing';
|
export * from './testing';
|
||||||
export * from './design';
|
export * from './design';
|
||||||
export * from './ai-native';
|
export * from './ai-native';
|
||||||
|
export * from './chatColors';
|
||||||
export * from './custom';
|
export * from './custom';
|
||||||
|
|
Loading…
Reference in New Issue