@opensumi/ide-ai-native 3.8.3-next-1745309832.0 → 3.8.3-next-1745568063.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/chat/chat.view.d.ts.map +1 -1
- package/lib/browser/chat/chat.view.js +18 -7
- package/lib/browser/chat/chat.view.js.map +1 -1
- package/lib/browser/components/ChatMentionInput.d.ts +2 -0
- package/lib/browser/components/ChatMentionInput.d.ts.map +1 -1
- package/lib/browser/components/ChatMentionInput.js +69 -23
- package/lib/browser/components/ChatMentionInput.js.map +1 -1
- package/lib/browser/components/mention-input/mention-input.d.ts.map +1 -1
- package/lib/browser/components/mention-input/mention-input.js +47 -10
- package/lib/browser/components/mention-input/mention-input.js.map +1 -1
- package/lib/browser/components/mention-input/mention-input.module.less +78 -6
- package/lib/browser/components/mention-input/types.d.ts +5 -0
- package/lib/browser/components/mention-input/types.d.ts.map +1 -1
- package/lib/browser/components/mention-input/types.js.map +1 -1
- package/lib/browser/context/llm-context.service.d.ts +1 -0
- package/lib/browser/context/llm-context.service.d.ts.map +1 -1
- package/lib/browser/context/llm-context.service.js.map +1 -1
- package/lib/browser/types.d.ts +2 -0
- package/lib/browser/types.d.ts.map +1 -1
- package/lib/browser/types.js.map +1 -1
- package/lib/common/llm-context.d.ts +1 -0
- package/lib/common/llm-context.d.ts.map +1 -1
- package/lib/common/llm-context.js.map +1 -1
- package/package.json +24 -23
- package/src/browser/chat/chat.view.tsx +20 -5
- package/src/browser/components/ChatMentionInput.tsx +85 -24
- package/src/browser/components/mention-input/mention-input.module.less +78 -6
- package/src/browser/components/mention-input/mention-input.tsx +86 -12
- package/src/browser/components/mention-input/types.ts +7 -0
- package/src/browser/context/llm-context.service.ts +1 -0
- package/src/browser/types.ts +2 -0
- package/src/common/llm-context.ts +6 -3
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { DataContent } from 'ai';
|
|
2
|
-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
|
|
4
4
|
import { Image } from '@opensumi/ide-components/lib/image';
|
|
5
|
-
import { LabelService, RecentFilesManager, useInjectable } from '@opensumi/ide-core-browser';
|
|
5
|
+
import { LabelService, RecentFilesManager, getSymbolIcon, useInjectable } from '@opensumi/ide-core-browser';
|
|
6
6
|
import { Icon, getIcon } from '@opensumi/ide-core-browser/lib/components';
|
|
7
7
|
import { ChatFeatureRegistryToken, URI, localize } from '@opensumi/ide-core-common';
|
|
8
8
|
import { CommandService } from '@opensumi/ide-core-common/lib/command';
|
|
9
9
|
import { defaultFilesWatcherExcludes } from '@opensumi/ide-core-common/lib/preferences/file-watch';
|
|
10
10
|
import { WorkbenchEditorService } from '@opensumi/ide-editor';
|
|
11
11
|
import { FileSearchServicePath, IFileSearchService } from '@opensumi/ide-file-search';
|
|
12
|
+
import { OutlineCompositeTreeNode, OutlineTreeNode } from '@opensumi/ide-outline/lib/browser/outline-node.define';
|
|
13
|
+
import { OutlineTreeService } from '@opensumi/ide-outline/lib/browser/services/outline-tree.service';
|
|
12
14
|
import { IMessageService } from '@opensumi/ide-overlay';
|
|
13
15
|
import { IWorkspaceService } from '@opensumi/ide-workspace';
|
|
14
16
|
|
|
15
17
|
import { IChatInternalService } from '../../common';
|
|
18
|
+
import { LLMContextService } from '../../common/llm-context';
|
|
16
19
|
import { ChatFeatureRegistry } from '../chat/chat.feature.registry';
|
|
17
20
|
import { ChatInternalService } from '../chat/chat.internal.service';
|
|
18
21
|
import { OPEN_MCP_CONFIG_COMMAND } from '../mcp/config/mcp-config.commands';
|
|
@@ -48,11 +51,11 @@ export interface IChatMentionInputProps {
|
|
|
48
51
|
setCommand: (command: string) => void;
|
|
49
52
|
disableModelSelector?: boolean;
|
|
50
53
|
sessionModelId?: string;
|
|
54
|
+
contextService?: LLMContextService;
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
// 指令命令激活组件
|
|
54
57
|
export const ChatMentionInput = (props: IChatMentionInputProps) => {
|
|
55
|
-
const { onSend, disabled = false } = props;
|
|
58
|
+
const { onSend, disabled = false, contextService } = props;
|
|
56
59
|
|
|
57
60
|
const [value, setValue] = useState(props.value || '');
|
|
58
61
|
const [images, setImages] = useState(props.images || []);
|
|
@@ -65,6 +68,8 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
|
|
|
65
68
|
const labelService = useInjectable<LabelService>(LabelService);
|
|
66
69
|
const messageService = useInjectable<IMessageService>(IMessageService);
|
|
67
70
|
const chatFeatureRegistry = useInjectable<ChatFeatureRegistry>(ChatFeatureRegistryToken);
|
|
71
|
+
const outlineTreeService = useInjectable<OutlineTreeService>(OutlineTreeService);
|
|
72
|
+
const prevOutlineItems = useRef<MentionItem[]>([]);
|
|
68
73
|
const handleShowMCPConfig = React.useCallback(() => {
|
|
69
74
|
commandService.executeCommand(OPEN_MCP_CONFIG_COMMAND.id);
|
|
70
75
|
}, [commandService]);
|
|
@@ -75,27 +80,76 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
|
|
|
75
80
|
}
|
|
76
81
|
}, [props.value]);
|
|
77
82
|
|
|
83
|
+
const resolveSymbols = useCallback(
|
|
84
|
+
async (parent?: OutlineCompositeTreeNode, symbols: (OutlineTreeNode | OutlineCompositeTreeNode)[] = []) => {
|
|
85
|
+
if (!parent) {
|
|
86
|
+
parent = (await outlineTreeService.resolveChildren())[0] as OutlineCompositeTreeNode;
|
|
87
|
+
}
|
|
88
|
+
const children = (await outlineTreeService.resolveChildren(parent)) as (
|
|
89
|
+
| OutlineTreeNode
|
|
90
|
+
| OutlineCompositeTreeNode
|
|
91
|
+
)[];
|
|
92
|
+
for (const child of children) {
|
|
93
|
+
symbols.push(child);
|
|
94
|
+
if (OutlineCompositeTreeNode.is(child)) {
|
|
95
|
+
await resolveSymbols(child, symbols);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return symbols;
|
|
99
|
+
},
|
|
100
|
+
[outlineTreeService],
|
|
101
|
+
);
|
|
102
|
+
|
|
78
103
|
// 默认菜单项
|
|
79
104
|
const defaultMenuItems: MentionItem[] = [
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
{
|
|
106
|
+
id: 'code',
|
|
107
|
+
type: 'code',
|
|
108
|
+
text: 'Code',
|
|
109
|
+
icon: getIcon('codebraces'),
|
|
110
|
+
getHighestLevelItems: () => [],
|
|
111
|
+
getItems: async (searchText: string) => {
|
|
112
|
+
if (!searchText || prevOutlineItems.current.length === 0) {
|
|
113
|
+
const uri = outlineTreeService.currentUri;
|
|
114
|
+
if (!uri) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
const treeNodes = await resolveSymbols();
|
|
118
|
+
prevOutlineItems.current = await Promise.all(
|
|
119
|
+
treeNodes.map(async (treeNode) => {
|
|
120
|
+
const relativePath = await workspaceService.asRelativePath(uri);
|
|
121
|
+
return {
|
|
122
|
+
id: treeNode.raw.id,
|
|
123
|
+
type: MentionType.CODE,
|
|
124
|
+
text: treeNode.raw.name,
|
|
125
|
+
symbol: treeNode.raw,
|
|
126
|
+
value: treeNode.raw.id,
|
|
127
|
+
description: `${relativePath?.root ? relativePath.path : ''}:L${treeNode.raw.range.startLineNumber}-${
|
|
128
|
+
treeNode.raw.range.endLineNumber
|
|
129
|
+
}`,
|
|
130
|
+
kind: treeNode.raw.kind,
|
|
131
|
+
contextId: `${outlineTreeService.currentUri?.codeUri.fsPath}:L${treeNode.raw.range.startLineNumber}-${treeNode.raw.range.endLineNumber}`,
|
|
132
|
+
icon: getSymbolIcon(treeNode.raw.kind) + ' outline-icon',
|
|
133
|
+
};
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
return prevOutlineItems.current;
|
|
137
|
+
} else {
|
|
138
|
+
searchText = searchText.toLocaleLowerCase();
|
|
139
|
+
return prevOutlineItems.current.sort((a, b) => {
|
|
140
|
+
if (a.text.toLocaleLowerCase().includes(searchText) && b.text.toLocaleLowerCase().includes(searchText)) {
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
if (a.text.toLocaleLowerCase().includes(searchText)) {
|
|
144
|
+
return -1;
|
|
145
|
+
} else if (b.text.toLocaleLowerCase().includes(searchText)) {
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
return 0;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
},
|
|
99
153
|
{
|
|
100
154
|
id: MentionType.FILE,
|
|
101
155
|
type: MentionType.FILE,
|
|
@@ -194,7 +248,13 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
|
|
|
194
248
|
let folders: MentionItem[] = [];
|
|
195
249
|
if (!searchText) {
|
|
196
250
|
const recentFile = await recentFilesManager.getMostRecentlyOpenedFiles();
|
|
197
|
-
const recentFolder = Array.from(
|
|
251
|
+
const recentFolder = Array.from(
|
|
252
|
+
new Set(
|
|
253
|
+
recentFile
|
|
254
|
+
.map((file) => new URI(file).parent.codeUri.fsPath)
|
|
255
|
+
.filter((folder) => folder !== workspaceService.workspace?.uri.toString() && folder !== '/'),
|
|
256
|
+
),
|
|
257
|
+
);
|
|
198
258
|
folders = await Promise.all(
|
|
199
259
|
recentFolder.map(async (folder) => {
|
|
200
260
|
const uri = new URI(folder);
|
|
@@ -354,6 +414,7 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
|
|
|
354
414
|
placeholder={localize('aiNative.chat.input.placeholder.default')}
|
|
355
415
|
footerConfig={defaultMentionInputFooterOptions}
|
|
356
416
|
onImageUpload={handleImageUpload}
|
|
417
|
+
contextService={contextService}
|
|
357
418
|
/>
|
|
358
419
|
</div>
|
|
359
420
|
);
|
|
@@ -12,8 +12,6 @@
|
|
|
12
12
|
.editor_area {
|
|
13
13
|
position: relative;
|
|
14
14
|
padding: 0 15px;
|
|
15
|
-
min-height: 42px;
|
|
16
|
-
max-height: 105px;
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
.editor {
|
|
@@ -21,11 +19,11 @@
|
|
|
21
19
|
background-color: transparent;
|
|
22
20
|
border: none;
|
|
23
21
|
font-size: 14px;
|
|
24
|
-
line-height:
|
|
22
|
+
line-height: 24px;
|
|
23
|
+
min-height: 72px;
|
|
24
|
+
max-height: 120px;
|
|
25
25
|
outline: none;
|
|
26
26
|
resize: none;
|
|
27
|
-
min-height: 24px;
|
|
28
|
-
max-height: 120px;
|
|
29
27
|
overflow-y: auto;
|
|
30
28
|
border-radius: 4px;
|
|
31
29
|
word-break: break-word;
|
|
@@ -142,6 +140,43 @@
|
|
|
142
140
|
display: flex;
|
|
143
141
|
justify-content: center;
|
|
144
142
|
}
|
|
143
|
+
.context_container {
|
|
144
|
+
display: flex;
|
|
145
|
+
align-items: center;
|
|
146
|
+
justify-content: center;
|
|
147
|
+
cursor: pointer;
|
|
148
|
+
.context_icon {
|
|
149
|
+
flex-grow: 0;
|
|
150
|
+
flex-shrink: 0;
|
|
151
|
+
:global(.kt-icon) {
|
|
152
|
+
font-size: 12px;
|
|
153
|
+
}
|
|
154
|
+
:global(.kticon-close) {
|
|
155
|
+
display: none;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
&:hover {
|
|
159
|
+
.context_icon {
|
|
160
|
+
:global(.kticon-close) {
|
|
161
|
+
display: block;
|
|
162
|
+
}
|
|
163
|
+
:global(.kticon-out-link) {
|
|
164
|
+
display: none;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
.context_description {
|
|
169
|
+
flex: 1;
|
|
170
|
+
margin-left: 3px;
|
|
171
|
+
margin-right: 10px;
|
|
172
|
+
text-align: left;
|
|
173
|
+
font-size: 11px;
|
|
174
|
+
overflow: hidden;
|
|
175
|
+
text-overflow: ellipsis;
|
|
176
|
+
white-space: nowrap;
|
|
177
|
+
color: var(--descriptionForeground);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
145
180
|
|
|
146
181
|
.mention_panel {
|
|
147
182
|
background-color: var(--editor-background);
|
|
@@ -191,6 +226,7 @@
|
|
|
191
226
|
|
|
192
227
|
.mention_item_left {
|
|
193
228
|
display: flex;
|
|
229
|
+
max-width: 100%;
|
|
194
230
|
align-items: center;
|
|
195
231
|
flex: 1;
|
|
196
232
|
}
|
|
@@ -198,7 +234,8 @@
|
|
|
198
234
|
.mention_item_icon {
|
|
199
235
|
margin-right: 8px;
|
|
200
236
|
width: 18px;
|
|
201
|
-
height:
|
|
237
|
+
height: 22px;
|
|
238
|
+
line-height: 22px !important;
|
|
202
239
|
display: flex;
|
|
203
240
|
align-items: center;
|
|
204
241
|
justify-content: center;
|
|
@@ -216,6 +253,7 @@
|
|
|
216
253
|
font-size: 13px;
|
|
217
254
|
display: inline;
|
|
218
255
|
flex: 1;
|
|
256
|
+
direction: rtl;
|
|
219
257
|
text-overflow: ellipsis;
|
|
220
258
|
overflow: hidden;
|
|
221
259
|
white-space: nowrap;
|
|
@@ -236,6 +274,40 @@
|
|
|
236
274
|
align-items: center;
|
|
237
275
|
}
|
|
238
276
|
|
|
277
|
+
.context_item {
|
|
278
|
+
display: flex;
|
|
279
|
+
align-items: center;
|
|
280
|
+
background-color: var(--badge-background);
|
|
281
|
+
color: var(--badge-foreground);
|
|
282
|
+
border-radius: 4px;
|
|
283
|
+
padding: 2px 6px;
|
|
284
|
+
font-size: 12px;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.context_item_icon {
|
|
288
|
+
margin-right: 4px;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.context_item_text {
|
|
292
|
+
margin-right: 4px;
|
|
293
|
+
max-width: 150px;
|
|
294
|
+
overflow: hidden;
|
|
295
|
+
text-overflow: ellipsis;
|
|
296
|
+
white-space: nowrap;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.context_item_remove {
|
|
300
|
+
cursor: pointer;
|
|
301
|
+
font-size: 12px;
|
|
302
|
+
display: flex;
|
|
303
|
+
align-items: center;
|
|
304
|
+
opacity: 0.7;
|
|
305
|
+
|
|
306
|
+
&:hover {
|
|
307
|
+
opacity: 1;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
239
311
|
.back_button {
|
|
240
312
|
background: none;
|
|
241
313
|
border: none;
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import cls from 'classnames';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { formatLocalize, getSymbolIcon, localize } from '@opensumi/ide-core-browser';
|
|
5
|
+
import { Icon, Popover, PopoverPosition, Select, getIcon } from '@opensumi/ide-core-browser/lib/components';
|
|
5
6
|
import { EnhanceIcon } from '@opensumi/ide-core-browser/lib/components/ai-native';
|
|
6
7
|
import { URI } from '@opensumi/ide-utils';
|
|
7
8
|
|
|
9
|
+
import { FileContext } from '../../../common/llm-context';
|
|
10
|
+
|
|
8
11
|
import styles from './mention-input.module.less';
|
|
9
12
|
import { MentionPanel } from './mention-panel';
|
|
10
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
FooterButtonPosition,
|
|
15
|
+
MENTION_KEYWORD,
|
|
16
|
+
MentionInputProps,
|
|
17
|
+
MentionItem,
|
|
18
|
+
MentionState,
|
|
19
|
+
MentionType,
|
|
20
|
+
} from './types';
|
|
11
21
|
|
|
12
22
|
export const WHITE_SPACE_TEXT = ' ';
|
|
13
23
|
|
|
@@ -26,6 +36,7 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
26
36
|
buttons: [],
|
|
27
37
|
showModelSelector: false,
|
|
28
38
|
},
|
|
39
|
+
contextService,
|
|
29
40
|
}) => {
|
|
30
41
|
const editorRef = React.useRef<HTMLDivElement>(null);
|
|
31
42
|
const [mentionState, setMentionState] = React.useState<MentionState>({
|
|
@@ -53,8 +64,14 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
53
64
|
const [historyIndex, setHistoryIndex] = React.useState<number>(-1);
|
|
54
65
|
const [currentInput, setCurrentInput] = React.useState<string>('');
|
|
55
66
|
const [isNavigatingHistory, setIsNavigatingHistory] = React.useState<boolean>(false);
|
|
67
|
+
const [attachedFiles, setAttachedFiles] = React.useState<{
|
|
68
|
+
files: FileContext[];
|
|
69
|
+
folders: FileContext[];
|
|
70
|
+
}>({
|
|
71
|
+
files: [],
|
|
72
|
+
folders: [],
|
|
73
|
+
});
|
|
56
74
|
|
|
57
|
-
// 获取当前菜单项
|
|
58
75
|
const getCurrentItems = (): MentionItem[] => {
|
|
59
76
|
if (mentionState.level === 0) {
|
|
60
77
|
return mentionItems;
|
|
@@ -70,7 +87,6 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
70
87
|
return [];
|
|
71
88
|
};
|
|
72
89
|
|
|
73
|
-
// 添加防抖函数
|
|
74
90
|
const useDebounce = <T,>(value: T, delay: number): T => {
|
|
75
91
|
const [debouncedValue, setDebouncedValue] = React.useState<T>(value);
|
|
76
92
|
|
|
@@ -87,14 +103,12 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
87
103
|
return debouncedValue;
|
|
88
104
|
};
|
|
89
105
|
|
|
90
|
-
// 使用防抖处理搜索文本
|
|
91
106
|
const debouncedSecondLevelFilter = useDebounce(mentionState.secondLevelFilter, 300);
|
|
92
107
|
|
|
93
108
|
React.useEffect(() => {
|
|
94
109
|
setSelectedModel(footerConfig.defaultModel || '');
|
|
95
110
|
}, [footerConfig.defaultModel]);
|
|
96
111
|
|
|
97
|
-
// 监听搜索文本变化,实时更新二级菜单
|
|
98
112
|
React.useEffect(() => {
|
|
99
113
|
if (mentionState.level === 1 && mentionState.parentType && debouncedSecondLevelFilter !== undefined) {
|
|
100
114
|
// 查找父级菜单项
|
|
@@ -162,6 +176,16 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
162
176
|
}
|
|
163
177
|
}, [debouncedSecondLevelFilter, mentionState.level, mentionState.parentType]);
|
|
164
178
|
|
|
179
|
+
React.useEffect(() => {
|
|
180
|
+
const disposable = contextService?.onDidContextFilesChangeEvent(({ attached, attachedFolders }) => {
|
|
181
|
+
setAttachedFiles({ files: attached, folders: attachedFolders });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return () => {
|
|
185
|
+
disposable?.dispose();
|
|
186
|
+
};
|
|
187
|
+
}, [contextService]);
|
|
188
|
+
|
|
165
189
|
// 获取光标位置
|
|
166
190
|
const getCursorPosition = (element: HTMLElement): number => {
|
|
167
191
|
const selection = window.getSelection();
|
|
@@ -176,7 +200,6 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
176
200
|
return preCaretRange.toString().length;
|
|
177
201
|
};
|
|
178
202
|
|
|
179
|
-
// 处理输入事件
|
|
180
203
|
const handleInput = () => {
|
|
181
204
|
// 如果用户开始输入,退出历史导航模式
|
|
182
205
|
if (isNavigatingHistory) {
|
|
@@ -262,7 +285,7 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
262
285
|
// 检查输入框高度,如果超过最大高度则添加滚动条
|
|
263
286
|
if (editorRef.current) {
|
|
264
287
|
const editorHeight = editorRef.current.scrollHeight;
|
|
265
|
-
if (editorHeight
|
|
288
|
+
if (editorHeight >= 120) {
|
|
266
289
|
editorRef.current.style.overflowY = 'auto';
|
|
267
290
|
} else {
|
|
268
291
|
editorRef.current.style.overflowY = 'hidden';
|
|
@@ -305,6 +328,15 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
305
328
|
return;
|
|
306
329
|
}
|
|
307
330
|
|
|
331
|
+
// 当输入框为空时,处理删除键 (Backspace) 或 Delete 键来删除上下文内容
|
|
332
|
+
if (
|
|
333
|
+
(e.key === 'Backspace' || e.key === 'Delete') &&
|
|
334
|
+
editorRef.current &&
|
|
335
|
+
(!editorRef.current.textContent || editorRef.current.textContent.trim() === '')
|
|
336
|
+
) {
|
|
337
|
+
contextService?.cleanFileContext();
|
|
338
|
+
}
|
|
339
|
+
|
|
308
340
|
// 添加对 @ 键的监听,支持在任意位置触发菜单
|
|
309
341
|
if (e.key === MENTION_KEYWORD && !mentionState.active && !mentionState.inlineSearchActive && editorRef.current) {
|
|
310
342
|
const cursorPos = getCursorPosition(editorRef.current);
|
|
@@ -665,15 +697,30 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
665
697
|
mentionTag.dataset.contextId = item.contextId || '';
|
|
666
698
|
mentionTag.contentEditable = 'false';
|
|
667
699
|
|
|
668
|
-
|
|
669
|
-
if (item.type === 'file' || item.type === 'folder') {
|
|
700
|
+
if (item.type === MentionType.FILE || item.type === MentionType.FOLDER) {
|
|
670
701
|
// 创建图标容器
|
|
671
702
|
const iconSpan = document.createElement('span');
|
|
672
703
|
iconSpan.className = cls(
|
|
673
704
|
styles.mention_icon,
|
|
674
|
-
item.type ===
|
|
705
|
+
item.type === MentionType.FILE ? labelService?.getIcon(new URI(item.text)) : getIcon('folder'),
|
|
675
706
|
);
|
|
676
707
|
mentionTag.appendChild(iconSpan);
|
|
708
|
+
if (item.type === MentionType.FOLDER) {
|
|
709
|
+
contextService?.addFolderToContext(new URI(item.contextId), true);
|
|
710
|
+
} else {
|
|
711
|
+
contextService?.addFileToContext(new URI(item.contextId), undefined, true);
|
|
712
|
+
}
|
|
713
|
+
} else if (item.type === MentionType.CODE) {
|
|
714
|
+
const iconSpan = document.createElement('span');
|
|
715
|
+
iconSpan.className = cls(styles.mention_icon, item.kind && getSymbolIcon(item.kind) + ' outline-icon');
|
|
716
|
+
mentionTag.appendChild(iconSpan);
|
|
717
|
+
if (item.symbol) {
|
|
718
|
+
contextService?.addFileToContext(
|
|
719
|
+
new URI(item.contextId),
|
|
720
|
+
[item.symbol.range.startLineNumber, item.symbol.range.endLineNumber],
|
|
721
|
+
true,
|
|
722
|
+
);
|
|
723
|
+
}
|
|
677
724
|
}
|
|
678
725
|
const workspace = workspaceService?.workspace;
|
|
679
726
|
let relativePath = item.text;
|
|
@@ -913,7 +960,6 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
913
960
|
setHistoryIndex(-1);
|
|
914
961
|
setIsNavigatingHistory(false);
|
|
915
962
|
}
|
|
916
|
-
|
|
917
963
|
if (onSend) {
|
|
918
964
|
// 传递当前选择的模型和其他配置信息
|
|
919
965
|
onSend(processedContent, {
|
|
@@ -931,6 +977,10 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
931
977
|
}
|
|
932
978
|
};
|
|
933
979
|
|
|
980
|
+
const handleClearContext = React.useCallback(() => {
|
|
981
|
+
contextService?.cleanFileContext();
|
|
982
|
+
}, [contextService]);
|
|
983
|
+
|
|
934
984
|
const handleStop = React.useCallback(() => {
|
|
935
985
|
if (onStop) {
|
|
936
986
|
onStop();
|
|
@@ -962,6 +1012,11 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
962
1012
|
[footerConfig.buttons],
|
|
963
1013
|
);
|
|
964
1014
|
|
|
1015
|
+
const hasContext = React.useMemo(
|
|
1016
|
+
() => attachedFiles.files.length > 0 || attachedFiles.folders.length > 0,
|
|
1017
|
+
[attachedFiles],
|
|
1018
|
+
);
|
|
1019
|
+
|
|
965
1020
|
return (
|
|
966
1021
|
<div className={styles.input_container}>
|
|
967
1022
|
{mentionState.active && (
|
|
@@ -1005,6 +1060,25 @@ export const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
1005
1060
|
</div>
|
|
1006
1061
|
<div className={styles.right_control}>
|
|
1007
1062
|
{renderButtons(FooterButtonPosition.RIGHT)}
|
|
1063
|
+
<Popover
|
|
1064
|
+
overlayClassName={styles.popover_icon}
|
|
1065
|
+
id={'ai-chat-clear-context'}
|
|
1066
|
+
position={PopoverPosition.top}
|
|
1067
|
+
content={localize('aiNative.chat.context.clear')}
|
|
1068
|
+
>
|
|
1069
|
+
<div className={styles.context_container} onClick={handleClearContext}>
|
|
1070
|
+
<div className={styles.context_icon}>
|
|
1071
|
+
<Icon icon='out-link' />
|
|
1072
|
+
<Icon icon='close' />
|
|
1073
|
+
</div>
|
|
1074
|
+
<div className={styles.context_description}>
|
|
1075
|
+
{formatLocalize(
|
|
1076
|
+
'aiNative.chat.context.description',
|
|
1077
|
+
attachedFiles.files.length + attachedFiles.folders.length,
|
|
1078
|
+
)}
|
|
1079
|
+
</div>
|
|
1080
|
+
</div>
|
|
1081
|
+
</Popover>
|
|
1008
1082
|
<Popover
|
|
1009
1083
|
overlayClassName={styles.popover_icon}
|
|
1010
1084
|
id={'ai-chat-send'}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { DocumentSymbol, SymbolKind } from '@opensumi/ide-monaco';
|
|
2
|
+
|
|
3
|
+
import { LLMContextService } from '../../../common/llm-context';
|
|
4
|
+
|
|
1
5
|
import type { LabelService } from '@opensumi/ide-core-browser';
|
|
2
6
|
import type { IWorkspaceService } from '@opensumi/ide-workspace';
|
|
3
7
|
|
|
@@ -8,7 +12,9 @@ export interface MentionItem {
|
|
|
8
12
|
value?: string;
|
|
9
13
|
description?: string;
|
|
10
14
|
contextId?: string;
|
|
15
|
+
symbol?: DocumentSymbol;
|
|
11
16
|
icon?: string;
|
|
17
|
+
kind?: SymbolKind;
|
|
12
18
|
getHighestLevelItems?: () => MentionItem[];
|
|
13
19
|
getItems?: (searchText: string) => Promise<MentionItem[]>;
|
|
14
20
|
}
|
|
@@ -82,6 +88,7 @@ export interface MentionInputProps {
|
|
|
82
88
|
mentionKeyword?: string;
|
|
83
89
|
labelService?: LabelService;
|
|
84
90
|
workspaceService?: IWorkspaceService;
|
|
91
|
+
contextService?: LLMContextService;
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
export const MENTION_KEYWORD = '@';
|
|
@@ -45,6 +45,7 @@ export class LLMContextServiceImpl extends WithEventBus implements LLMContextSer
|
|
|
45
45
|
private readonly onDidContextFilesChangeEmitter = new Emitter<{
|
|
46
46
|
viewed: FileContext[];
|
|
47
47
|
attached: FileContext[];
|
|
48
|
+
attachedFolders: FileContext[];
|
|
48
49
|
version: number;
|
|
49
50
|
}>();
|
|
50
51
|
onDidContextFilesChangeEvent = this.onDidContextFilesChangeEmitter.event;
|
package/src/browser/types.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { SumiReadableStream } from '@opensumi/ide-utils/lib/stream';
|
|
|
28
28
|
import { IMarker } from '@opensumi/monaco-editor-core/esm/vs/platform/markers/common/markers';
|
|
29
29
|
|
|
30
30
|
import { IChatWelcomeMessageContent, ISampleQuestions, ITerminalCommandSuggestionDesc } from '../common';
|
|
31
|
+
import { LLMContextService } from '../common/llm-context';
|
|
31
32
|
|
|
32
33
|
import {
|
|
33
34
|
ICodeEditsContextBean,
|
|
@@ -166,6 +167,7 @@ export type ChatInputRender = (props: {
|
|
|
166
167
|
defaultAgentId?: string;
|
|
167
168
|
command: string;
|
|
168
169
|
setCommand: (theme: string) => void;
|
|
170
|
+
contextService?: LLMContextService;
|
|
169
171
|
}) => React.ReactElement | React.JSX.Element;
|
|
170
172
|
export type ChatViewHeaderRender = (props: {
|
|
171
173
|
handleClear: () => any;
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { DataContent } from 'ai';
|
|
2
|
-
|
|
3
1
|
import { Event, URI } from '@opensumi/ide-core-common/lib/utils';
|
|
4
2
|
|
|
5
3
|
export interface LLMContextService {
|
|
@@ -31,7 +29,12 @@ export interface LLMContextService {
|
|
|
31
29
|
/**
|
|
32
30
|
* 上下文文件变化事件
|
|
33
31
|
*/
|
|
34
|
-
onDidContextFilesChangeEvent: Event<{
|
|
32
|
+
onDidContextFilesChangeEvent: Event<{
|
|
33
|
+
viewed: FileContext[];
|
|
34
|
+
attached: FileContext[];
|
|
35
|
+
attachedFolders: FileContext[];
|
|
36
|
+
version: number;
|
|
37
|
+
}>;
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
40
|
* 从 context 中移除文件
|