mirror of https://github.com/opensumi/core
feat: support code reference and reference bar (#4519)
* style: improve overflow style * feat: support code reference and showing references * fix: build * chore: update lock * chore: delete context by keybinding
This commit is contained in:
parent
24334e43cd
commit
909296f03c
|
@ -162,6 +162,9 @@
|
|||
{
|
||||
"path": "./references/tsconfig.addons.json"
|
||||
},
|
||||
{
|
||||
"path": "./references/tsconfig.outline.json"
|
||||
},
|
||||
{
|
||||
"path": "./references/tsconfig.ai-native.json"
|
||||
},
|
||||
|
@ -174,9 +177,6 @@
|
|||
{
|
||||
"path": "./references/tsconfig.keymaps.json"
|
||||
},
|
||||
{
|
||||
"path": "./references/tsconfig.outline.json"
|
||||
},
|
||||
{
|
||||
"path": "./references/tsconfig.startup.json"
|
||||
},
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
"@opensumi/ide-main-layout": "workspace:*",
|
||||
"@opensumi/ide-markers": "workspace:*",
|
||||
"@opensumi/ide-monaco": "workspace:*",
|
||||
"@opensumi/ide-outline": "workspace:*",
|
||||
"@opensumi/ide-overlay": "workspace:*",
|
||||
"@opensumi/ide-preferences": "workspace:*",
|
||||
"@opensumi/ide-search": "workspace:*",
|
||||
|
|
|
@ -701,10 +701,7 @@ export const AIChatView = () => {
|
|||
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}\``);
|
||||
}
|
||||
}
|
||||
|
@ -715,12 +712,29 @@ export const AIChatView = () => {
|
|||
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}\``);
|
||||
}
|
||||
}
|
||||
const codePattern = /\{\{@code:(.*?)\}\}/g;
|
||||
const codeMatches = processedContent.match(codePattern);
|
||||
if (codeMatches) {
|
||||
for (const match of codeMatches) {
|
||||
const filePathWithLineRange = match.replace(/\{\{@code:(.*?)\}\}/, '$1');
|
||||
const [filePath, lineRange] = filePathWithLineRange.split(':');
|
||||
let range: [number, number] = [0, 0];
|
||||
if (lineRange) {
|
||||
const [startLine, endLine] = lineRange.slice(1).split('-');
|
||||
range = [parseInt(startLine, 10), parseInt(endLine, 10)];
|
||||
}
|
||||
const fileUri = new URI(filePath);
|
||||
const relativePath = (await workspaceService.asRelativePath(fileUri))?.path || fileUri.displayName;
|
||||
processedContent = processedContent.replace(
|
||||
match,
|
||||
`\`<attached_file>${relativePath}:L${range[0]}-${range[1]}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
return handleAgentReply({ message: processedContent, images, agentId, command, reportExtra });
|
||||
},
|
||||
[handleAgentReply],
|
||||
|
@ -879,6 +893,7 @@ export const AIChatView = () => {
|
|||
defaultAgentId={defaultAgentId}
|
||||
command={command}
|
||||
setCommand={setCommand}
|
||||
contextService={llmContextService}
|
||||
ref={chatInputRef}
|
||||
disableModelSelector={sessionModelId !== undefined || loading}
|
||||
sessionModelId={sessionModelId}
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
import { DataContent } from 'ai';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { Image } from '@opensumi/ide-components/lib/image';
|
||||
import { LabelService, RecentFilesManager, useInjectable } from '@opensumi/ide-core-browser';
|
||||
import { LabelService, RecentFilesManager, getSymbolIcon, useInjectable } from '@opensumi/ide-core-browser';
|
||||
import { Icon, getIcon } from '@opensumi/ide-core-browser/lib/components';
|
||||
import { ChatFeatureRegistryToken, URI, localize } from '@opensumi/ide-core-common';
|
||||
import { CommandService } from '@opensumi/ide-core-common/lib/command';
|
||||
import { defaultFilesWatcherExcludes } from '@opensumi/ide-core-common/lib/preferences/file-watch';
|
||||
import { WorkbenchEditorService } from '@opensumi/ide-editor';
|
||||
import { FileSearchServicePath, IFileSearchService } from '@opensumi/ide-file-search';
|
||||
import { OutlineCompositeTreeNode, OutlineTreeNode } from '@opensumi/ide-outline/lib/browser/outline-node.define';
|
||||
import { OutlineTreeService } from '@opensumi/ide-outline/lib/browser/services/outline-tree.service';
|
||||
import { IMessageService } from '@opensumi/ide-overlay';
|
||||
import { IWorkspaceService } from '@opensumi/ide-workspace';
|
||||
|
||||
import { IChatInternalService } from '../../common';
|
||||
import { LLMContextService } from '../../common/llm-context';
|
||||
import { ChatFeatureRegistry } from '../chat/chat.feature.registry';
|
||||
import { ChatInternalService } from '../chat/chat.internal.service';
|
||||
import { OPEN_MCP_CONFIG_COMMAND } from '../mcp/config/mcp-config.commands';
|
||||
|
@ -48,11 +51,11 @@ export interface IChatMentionInputProps {
|
|||
setCommand: (command: string) => void;
|
||||
disableModelSelector?: boolean;
|
||||
sessionModelId?: string;
|
||||
contextService?: LLMContextService;
|
||||
}
|
||||
|
||||
// 指令命令激活组件
|
||||
export const ChatMentionInput = (props: IChatMentionInputProps) => {
|
||||
const { onSend, disabled = false } = props;
|
||||
const { onSend, disabled = false, contextService } = props;
|
||||
|
||||
const [value, setValue] = useState(props.value || '');
|
||||
const [images, setImages] = useState(props.images || []);
|
||||
|
@ -65,6 +68,8 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
|
|||
const labelService = useInjectable<LabelService>(LabelService);
|
||||
const messageService = useInjectable<IMessageService>(IMessageService);
|
||||
const chatFeatureRegistry = useInjectable<ChatFeatureRegistry>(ChatFeatureRegistryToken);
|
||||
const outlineTreeService = useInjectable<OutlineTreeService>(OutlineTreeService);
|
||||
const prevOutlineItems = useRef<MentionItem[]>([]);
|
||||
const handleShowMCPConfig = React.useCallback(() => {
|
||||
commandService.executeCommand(OPEN_MCP_CONFIG_COMMAND.id);
|
||||
}, [commandService]);
|
||||
|
@ -75,27 +80,76 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
|
|||
}
|
||||
}, [props.value]);
|
||||
|
||||
const resolveSymbols = useCallback(
|
||||
async (parent?: OutlineCompositeTreeNode, symbols: (OutlineTreeNode | OutlineCompositeTreeNode)[] = []) => {
|
||||
if (!parent) {
|
||||
parent = (await outlineTreeService.resolveChildren())[0] as OutlineCompositeTreeNode;
|
||||
}
|
||||
const children = (await outlineTreeService.resolveChildren(parent)) as (
|
||||
| OutlineTreeNode
|
||||
| OutlineCompositeTreeNode
|
||||
)[];
|
||||
for (const child of children) {
|
||||
symbols.push(child);
|
||||
if (OutlineCompositeTreeNode.is(child)) {
|
||||
await resolveSymbols(child, symbols);
|
||||
}
|
||||
}
|
||||
return symbols;
|
||||
},
|
||||
[outlineTreeService],
|
||||
);
|
||||
|
||||
// 默认菜单项
|
||||
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: 'code',
|
||||
type: 'code',
|
||||
text: 'Code',
|
||||
icon: getIcon('codebraces'),
|
||||
getHighestLevelItems: () => [],
|
||||
getItems: async (searchText: string) => {
|
||||
if (!searchText || prevOutlineItems.current.length === 0) {
|
||||
const uri = outlineTreeService.currentUri;
|
||||
if (!uri) {
|
||||
return [];
|
||||
}
|
||||
const treeNodes = await resolveSymbols();
|
||||
prevOutlineItems.current = await Promise.all(
|
||||
treeNodes.map(async (treeNode) => {
|
||||
const relativePath = await workspaceService.asRelativePath(uri);
|
||||
return {
|
||||
id: treeNode.raw.id,
|
||||
type: MentionType.CODE,
|
||||
text: treeNode.raw.name,
|
||||
symbol: treeNode.raw,
|
||||
value: treeNode.raw.id,
|
||||
description: `${relativePath?.root ? relativePath.path : ''}:L${treeNode.raw.range.startLineNumber}-${
|
||||
treeNode.raw.range.endLineNumber
|
||||
}`,
|
||||
kind: treeNode.raw.kind,
|
||||
contextId: `${outlineTreeService.currentUri?.codeUri.fsPath}:L${treeNode.raw.range.startLineNumber}-${treeNode.raw.range.endLineNumber}`,
|
||||
icon: getSymbolIcon(treeNode.raw.kind) + ' outline-icon',
|
||||
};
|
||||
}),
|
||||
);
|
||||
return prevOutlineItems.current;
|
||||
} else {
|
||||
searchText = searchText.toLocaleLowerCase();
|
||||
return prevOutlineItems.current.sort((a, b) => {
|
||||
if (a.text.toLocaleLowerCase().includes(searchText) && b.text.toLocaleLowerCase().includes(searchText)) {
|
||||
return 0;
|
||||
}
|
||||
if (a.text.toLocaleLowerCase().includes(searchText)) {
|
||||
return -1;
|
||||
} else if (b.text.toLocaleLowerCase().includes(searchText)) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: MentionType.FILE,
|
||||
type: MentionType.FILE,
|
||||
|
@ -194,7 +248,13 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
|
|||
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)));
|
||||
const recentFolder = Array.from(
|
||||
new Set(
|
||||
recentFile
|
||||
.map((file) => new URI(file).parent.codeUri.fsPath)
|
||||
.filter((folder) => folder !== workspaceService.workspace?.uri.toString() && folder !== '/'),
|
||||
),
|
||||
);
|
||||
folders = await Promise.all(
|
||||
recentFolder.map(async (folder) => {
|
||||
const uri = new URI(folder);
|
||||
|
@ -354,6 +414,7 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
|
|||
placeholder={localize('aiNative.chat.input.placeholder.default')}
|
||||
footerConfig={defaultMentionInputFooterOptions}
|
||||
onImageUpload={handleImageUpload}
|
||||
contextService={contextService}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -12,8 +12,6 @@
|
|||
.editor_area {
|
||||
position: relative;
|
||||
padding: 0 15px;
|
||||
min-height: 42px;
|
||||
max-height: 105px;
|
||||
}
|
||||
|
||||
.editor {
|
||||
|
@ -21,11 +19,11 @@
|
|||
background-color: transparent;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
line-height: 24px;
|
||||
min-height: 72px;
|
||||
max-height: 120px;
|
||||
outline: none;
|
||||
resize: none;
|
||||
min-height: 24px;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
border-radius: 4px;
|
||||
word-break: break-word;
|
||||
|
@ -142,6 +140,43 @@
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.context_container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
.context_icon {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
:global(.kt-icon) {
|
||||
font-size: 12px;
|
||||
}
|
||||
:global(.kticon-close) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.context_icon {
|
||||
:global(.kticon-close) {
|
||||
display: block;
|
||||
}
|
||||
:global(.kticon-out-link) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.context_description {
|
||||
flex: 1;
|
||||
margin-left: 3px;
|
||||
margin-right: 10px;
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--descriptionForeground);
|
||||
}
|
||||
}
|
||||
|
||||
.mention_panel {
|
||||
background-color: var(--editor-background);
|
||||
|
@ -191,6 +226,7 @@
|
|||
|
||||
.mention_item_left {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
@ -198,7 +234,8 @@
|
|||
.mention_item_icon {
|
||||
margin-right: 8px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
height: 22px;
|
||||
line-height: 22px !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -216,6 +253,7 @@
|
|||
font-size: 13px;
|
||||
display: inline;
|
||||
flex: 1;
|
||||
direction: rtl;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
@ -236,6 +274,40 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.context_item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--badge-background);
|
||||
color: var(--badge-foreground);
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.context_item_icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.context_item_text {
|
||||
margin-right: 4px;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.context_item_remove {
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.back_button {
|
||||
background: none;
|
||||
border: none;
|
||||
|
|
|
@ -1,13 +1,23 @@
|
|||
import cls from 'classnames';
|
||||
import * as React from 'react';
|
||||
|
||||
import { Popover, PopoverPosition, Select, getIcon } from '@opensumi/ide-core-browser/lib/components';
|
||||
import { formatLocalize, getSymbolIcon, localize } from '@opensumi/ide-core-browser';
|
||||
import { Icon, 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 { FileContext } from '../../../common/llm-context';
|
||||
|
||||
import styles from './mention-input.module.less';
|
||||
import { MentionPanel } from './mention-panel';
|
||||
import { FooterButtonPosition, MENTION_KEYWORD, MentionInputProps, MentionItem, MentionState } from './types';
|
||||
import {
|
||||
FooterButtonPosition,
|
||||
MENTION_KEYWORD,
|
||||
MentionInputProps,
|
||||
MentionItem,
|
||||
MentionState,
|
||||
MentionType,
|
||||
} from './types';
|
||||
|
||||
export const WHITE_SPACE_TEXT = ' ';
|
||||
|
||||
|
@ -26,6 +36,7 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|||
buttons: [],
|
||||
showModelSelector: false,
|
||||
},
|
||||
contextService,
|
||||
}) => {
|
||||
const editorRef = React.useRef<HTMLDivElement>(null);
|
||||
const [mentionState, setMentionState] = React.useState<MentionState>({
|
||||
|
@ -53,8 +64,14 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|||
const [historyIndex, setHistoryIndex] = React.useState<number>(-1);
|
||||
const [currentInput, setCurrentInput] = React.useState<string>('');
|
||||
const [isNavigatingHistory, setIsNavigatingHistory] = React.useState<boolean>(false);
|
||||
const [attachedFiles, setAttachedFiles] = React.useState<{
|
||||
files: FileContext[];
|
||||
folders: FileContext[];
|
||||
}>({
|
||||
files: [],
|
||||
folders: [],
|
||||
});
|
||||
|
||||
// 获取当前菜单项
|
||||
const getCurrentItems = (): MentionItem[] => {
|
||||
if (mentionState.level === 0) {
|
||||
return mentionItems;
|
||||
|
@ -70,7 +87,6 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|||
return [];
|
||||
};
|
||||
|
||||
// 添加防抖函数
|
||||
const useDebounce = <T,>(value: T, delay: number): T => {
|
||||
const [debouncedValue, setDebouncedValue] = React.useState<T>(value);
|
||||
|
||||
|
@ -87,14 +103,12 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|||
return debouncedValue;
|
||||
};
|
||||
|
||||
// 使用防抖处理搜索文本
|
||||
const debouncedSecondLevelFilter = useDebounce(mentionState.secondLevelFilter, 300);
|
||||
|
||||
React.useEffect(() => {
|
||||
setSelectedModel(footerConfig.defaultModel || '');
|
||||
}, [footerConfig.defaultModel]);
|
||||
|
||||
// 监听搜索文本变化,实时更新二级菜单
|
||||
React.useEffect(() => {
|
||||
if (mentionState.level === 1 && mentionState.parentType && debouncedSecondLevelFilter !== undefined) {
|
||||
// 查找父级菜单项
|
||||
|
@ -162,6 +176,16 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|||
}
|
||||
}, [debouncedSecondLevelFilter, mentionState.level, mentionState.parentType]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const disposable = contextService?.onDidContextFilesChangeEvent(({ attached, attachedFolders }) => {
|
||||
setAttachedFiles({ files: attached, folders: attachedFolders });
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposable?.dispose();
|
||||
};
|
||||
}, [contextService]);
|
||||
|
||||
// 获取光标位置
|
||||
const getCursorPosition = (element: HTMLElement): number => {
|
||||
const selection = window.getSelection();
|
||||
|
@ -176,7 +200,6 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|||
return preCaretRange.toString().length;
|
||||
};
|
||||
|
||||
// 处理输入事件
|
||||
const handleInput = () => {
|
||||
// 如果用户开始输入,退出历史导航模式
|
||||
if (isNavigatingHistory) {
|
||||
|
@ -262,7 +285,7 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|||
// 检查输入框高度,如果超过最大高度则添加滚动条
|
||||
if (editorRef.current) {
|
||||
const editorHeight = editorRef.current.scrollHeight;
|
||||
if (editorHeight > 120) {
|
||||
if (editorHeight >= 120) {
|
||||
editorRef.current.style.overflowY = 'auto';
|
||||
} else {
|
||||
editorRef.current.style.overflowY = 'hidden';
|
||||
|
@ -305,6 +328,15 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
// 当输入框为空时,处理删除键 (Backspace) 或 Delete 键来删除上下文内容
|
||||
if (
|
||||
(e.key === 'Backspace' || e.key === 'Delete') &&
|
||||
editorRef.current &&
|
||||
(!editorRef.current.textContent || editorRef.current.textContent.trim() === '')
|
||||
) {
|
||||
contextService?.cleanFileContext();
|
||||
}
|
||||
|
||||
// 添加对 @ 键的监听,支持在任意位置触发菜单
|
||||
if (e.key === MENTION_KEYWORD && !mentionState.active && !mentionState.inlineSearchActive && editorRef.current) {
|
||||
const cursorPos = getCursorPosition(editorRef.current);
|
||||
|
@ -665,15 +697,30 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|||
mentionTag.dataset.contextId = item.contextId || '';
|
||||
mentionTag.contentEditable = 'false';
|
||||
|
||||
// 为 file 和 folder 类型添加图标
|
||||
if (item.type === 'file' || item.type === 'folder') {
|
||||
if (item.type === MentionType.FILE || item.type === MentionType.FOLDER) {
|
||||
// 创建图标容器
|
||||
const iconSpan = document.createElement('span');
|
||||
iconSpan.className = cls(
|
||||
styles.mention_icon,
|
||||
item.type === 'file' ? labelService?.getIcon(new URI(item.text)) : getIcon('folder'),
|
||||
item.type === MentionType.FILE ? labelService?.getIcon(new URI(item.text)) : getIcon('folder'),
|
||||
);
|
||||
mentionTag.appendChild(iconSpan);
|
||||
if (item.type === MentionType.FOLDER) {
|
||||
contextService?.addFolderToContext(new URI(item.contextId), true);
|
||||
} else {
|
||||
contextService?.addFileToContext(new URI(item.contextId), undefined, true);
|
||||
}
|
||||
} else if (item.type === MentionType.CODE) {
|
||||
const iconSpan = document.createElement('span');
|
||||
iconSpan.className = cls(styles.mention_icon, item.kind && getSymbolIcon(item.kind) + ' outline-icon');
|
||||
mentionTag.appendChild(iconSpan);
|
||||
if (item.symbol) {
|
||||
contextService?.addFileToContext(
|
||||
new URI(item.contextId),
|
||||
[item.symbol.range.startLineNumber, item.symbol.range.endLineNumber],
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
const workspace = workspaceService?.workspace;
|
||||
let relativePath = item.text;
|
||||
|
@ -913,7 +960,6 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|||
setHistoryIndex(-1);
|
||||
setIsNavigatingHistory(false);
|
||||
}
|
||||
|
||||
if (onSend) {
|
||||
// 传递当前选择的模型和其他配置信息
|
||||
onSend(processedContent, {
|
||||
|
@ -931,6 +977,10 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handleClearContext = React.useCallback(() => {
|
||||
contextService?.cleanFileContext();
|
||||
}, [contextService]);
|
||||
|
||||
const handleStop = React.useCallback(() => {
|
||||
if (onStop) {
|
||||
onStop();
|
||||
|
@ -962,6 +1012,11 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|||
[footerConfig.buttons],
|
||||
);
|
||||
|
||||
const hasContext = React.useMemo(
|
||||
() => attachedFiles.files.length > 0 || attachedFiles.folders.length > 0,
|
||||
[attachedFiles],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.input_container}>
|
||||
{mentionState.active && (
|
||||
|
@ -1005,6 +1060,25 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|||
</div>
|
||||
<div className={styles.right_control}>
|
||||
{renderButtons(FooterButtonPosition.RIGHT)}
|
||||
<Popover
|
||||
overlayClassName={styles.popover_icon}
|
||||
id={'ai-chat-clear-context'}
|
||||
position={PopoverPosition.top}
|
||||
content={localize('aiNative.chat.context.clear')}
|
||||
>
|
||||
<div className={styles.context_container} onClick={handleClearContext}>
|
||||
<div className={styles.context_icon}>
|
||||
<Icon icon='out-link' />
|
||||
<Icon icon='close' />
|
||||
</div>
|
||||
<div className={styles.context_description}>
|
||||
{formatLocalize(
|
||||
'aiNative.chat.context.description',
|
||||
attachedFiles.files.length + attachedFiles.folders.length,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
<Popover
|
||||
overlayClassName={styles.popover_icon}
|
||||
id={'ai-chat-send'}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import { DocumentSymbol, SymbolKind } from '@opensumi/ide-monaco';
|
||||
|
||||
import { LLMContextService } from '../../../common/llm-context';
|
||||
|
||||
import type { LabelService } from '@opensumi/ide-core-browser';
|
||||
import type { IWorkspaceService } from '@opensumi/ide-workspace';
|
||||
|
||||
|
@ -8,7 +12,9 @@ export interface MentionItem {
|
|||
value?: string;
|
||||
description?: string;
|
||||
contextId?: string;
|
||||
symbol?: DocumentSymbol;
|
||||
icon?: string;
|
||||
kind?: SymbolKind;
|
||||
getHighestLevelItems?: () => MentionItem[];
|
||||
getItems?: (searchText: string) => Promise<MentionItem[]>;
|
||||
}
|
||||
|
@ -82,6 +88,7 @@ export interface MentionInputProps {
|
|||
mentionKeyword?: string;
|
||||
labelService?: LabelService;
|
||||
workspaceService?: IWorkspaceService;
|
||||
contextService?: LLMContextService;
|
||||
}
|
||||
|
||||
export const MENTION_KEYWORD = '@';
|
||||
|
|
|
@ -45,6 +45,7 @@ export class LLMContextServiceImpl extends WithEventBus implements LLMContextSer
|
|||
private readonly onDidContextFilesChangeEmitter = new Emitter<{
|
||||
viewed: FileContext[];
|
||||
attached: FileContext[];
|
||||
attachedFolders: FileContext[];
|
||||
version: number;
|
||||
}>();
|
||||
onDidContextFilesChangeEvent = this.onDidContextFilesChangeEmitter.event;
|
||||
|
|
|
@ -28,6 +28,7 @@ import { SumiReadableStream } from '@opensumi/ide-utils/lib/stream';
|
|||
import { IMarker } from '@opensumi/monaco-editor-core/esm/vs/platform/markers/common/markers';
|
||||
|
||||
import { IChatWelcomeMessageContent, ISampleQuestions, ITerminalCommandSuggestionDesc } from '../common';
|
||||
import { LLMContextService } from '../common/llm-context';
|
||||
|
||||
import {
|
||||
ICodeEditsContextBean,
|
||||
|
@ -166,6 +167,7 @@ export type ChatInputRender = (props: {
|
|||
defaultAgentId?: string;
|
||||
command: string;
|
||||
setCommand: (theme: string) => void;
|
||||
contextService?: LLMContextService;
|
||||
}) => React.ReactElement | React.JSX.Element;
|
||||
export type ChatViewHeaderRender = (props: {
|
||||
handleClear: () => any;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { DataContent } from 'ai';
|
||||
|
||||
import { Event, URI } from '@opensumi/ide-core-common/lib/utils';
|
||||
|
||||
export interface LLMContextService {
|
||||
|
@ -31,7 +29,12 @@ export interface LLMContextService {
|
|||
/**
|
||||
* 上下文文件变化事件
|
||||
*/
|
||||
onDidContextFilesChangeEvent: Event<{ viewed: FileContext[]; attached: FileContext[]; version: number }>;
|
||||
onDidContextFilesChangeEvent: Event<{
|
||||
viewed: FileContext[];
|
||||
attached: FileContext[];
|
||||
attachedFolders: FileContext[];
|
||||
version: number;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* 从 context 中移除文件
|
||||
|
|
|
@ -1463,7 +1463,9 @@ export const localizationBundle = {
|
|||
'aiNative.chat.defaultContextFolder': 'Current Folder',
|
||||
'aiNative.chat.thinking': 'Deep Think',
|
||||
'aiNative.chat.imageUpload': 'Upload Image',
|
||||
|
||||
'aiNative.chat.clearContext': 'Clear Context',
|
||||
'aiNative.chat.context.description': 'Total {0} References',
|
||||
'aiNative.chat.context.clear': 'Clear References',
|
||||
'aiNative.inline.chat.operate.chat.title': 'Chat({0})',
|
||||
'aiNative.inline.chat.operate.check.title': 'Check',
|
||||
'aiNative.inline.chat.operate.thumbsup.title': 'Thumbs up',
|
||||
|
|
|
@ -1231,6 +1231,9 @@ export const localizationBundle = {
|
|||
'aiNative.chat.defaultContextFolder': '当前文件夹',
|
||||
'aiNative.chat.thinking': '深度思考',
|
||||
'aiNative.chat.imageUpload': '上传图片',
|
||||
'aiNative.chat.clearContext': '清空上下文',
|
||||
'aiNative.chat.context.description': '共 {0} 个引用',
|
||||
'aiNative.chat.context.clear': '点击清空引用',
|
||||
|
||||
'aiNative.inline.chat.operate.chat.title': 'Chat({0})',
|
||||
'aiNative.inline.chat.operate.check.title': '采纳',
|
||||
|
|
|
@ -3409,6 +3409,7 @@ __metadata:
|
|||
"@opensumi/ide-main-layout": "workspace:*"
|
||||
"@opensumi/ide-markers": "workspace:*"
|
||||
"@opensumi/ide-monaco": "workspace:*"
|
||||
"@opensumi/ide-outline": "workspace:*"
|
||||
"@opensumi/ide-overlay": "workspace:*"
|
||||
"@opensumi/ide-preferences": "workspace:*"
|
||||
"@opensumi/ide-search": "workspace:*"
|
||||
|
|
Loading…
Reference in New Issue