@opensumi/ide-ai-native 3.8.3-next-1742180589.0 → 3.8.3-next-1742193440.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.
Files changed (83) hide show
  1. package/lib/browser/chat/chat-manager.service.d.ts +1 -1
  2. package/lib/browser/chat/chat-manager.service.d.ts.map +1 -1
  3. package/lib/browser/chat/chat-manager.service.js +3 -2
  4. package/lib/browser/chat/chat-manager.service.js.map +1 -1
  5. package/lib/browser/chat/chat-model.d.ts.map +1 -1
  6. package/lib/browser/chat/chat-model.js +11 -2
  7. package/lib/browser/chat/chat-model.js.map +1 -1
  8. package/lib/browser/chat/chat-proxy.service.d.ts.map +1 -1
  9. package/lib/browser/chat/chat-proxy.service.js +1 -0
  10. package/lib/browser/chat/chat-proxy.service.js.map +1 -1
  11. package/lib/browser/chat/chat.feature.registry.d.ts +4 -1
  12. package/lib/browser/chat/chat.feature.registry.d.ts.map +1 -1
  13. package/lib/browser/chat/chat.feature.registry.js +6 -0
  14. package/lib/browser/chat/chat.feature.registry.js.map +1 -1
  15. package/lib/browser/chat/chat.internal.service.d.ts +1 -1
  16. package/lib/browser/chat/chat.internal.service.d.ts.map +1 -1
  17. package/lib/browser/chat/chat.internal.service.js +2 -2
  18. package/lib/browser/chat/chat.internal.service.js.map +1 -1
  19. package/lib/browser/chat/chat.view.d.ts.map +1 -1
  20. package/lib/browser/chat/chat.view.js +10 -7
  21. package/lib/browser/chat/chat.view.js.map +1 -1
  22. package/lib/browser/components/ChatEditor.d.ts +2 -1
  23. package/lib/browser/components/ChatEditor.d.ts.map +1 -1
  24. package/lib/browser/components/ChatEditor.js +5 -2
  25. package/lib/browser/components/ChatEditor.js.map +1 -1
  26. package/lib/browser/components/ChatInput.d.ts +1 -1
  27. package/lib/browser/components/ChatInput.d.ts.map +1 -1
  28. package/lib/browser/components/ChatInput.js +1 -1
  29. package/lib/browser/components/ChatInput.js.map +1 -1
  30. package/lib/browser/components/ChatMentionInput.d.ts +3 -1
  31. package/lib/browser/components/ChatMentionInput.d.ts.map +1 -1
  32. package/lib/browser/components/ChatMentionInput.js +54 -3
  33. package/lib/browser/components/ChatMentionInput.js.map +1 -1
  34. package/lib/browser/components/components.module.less +49 -0
  35. package/lib/browser/components/mention-input/mention-input.d.ts.map +1 -1
  36. package/lib/browser/components/mention-input/mention-input.js +124 -60
  37. package/lib/browser/components/mention-input/mention-input.js.map +1 -1
  38. package/lib/browser/components/mention-input/types.d.ts +3 -1
  39. package/lib/browser/components/mention-input/types.d.ts.map +1 -1
  40. package/lib/browser/components/mention-input/types.js.map +1 -1
  41. package/lib/browser/context/llm-context.service.d.ts.map +1 -1
  42. package/lib/browser/context/llm-context.service.js.map +1 -1
  43. package/lib/browser/mcp/base-apply.service.d.ts.map +1 -1
  44. package/lib/browser/mcp/base-apply.service.js +5 -6
  45. package/lib/browser/mcp/base-apply.service.js.map +1 -1
  46. package/lib/browser/mcp/config/components/mcp-config.view.js +3 -3
  47. package/lib/browser/mcp/config/components/mcp-config.view.js.map +1 -1
  48. package/lib/browser/model/msg-history-manager.d.ts +1 -1
  49. package/lib/browser/model/msg-history-manager.d.ts.map +1 -1
  50. package/lib/browser/model/msg-history-manager.js.map +1 -1
  51. package/lib/browser/types.d.ts +7 -1
  52. package/lib/browser/types.d.ts.map +1 -1
  53. package/lib/browser/types.js.map +1 -1
  54. package/lib/common/index.d.ts +6 -0
  55. package/lib/common/index.d.ts.map +1 -1
  56. package/lib/common/index.js.map +1 -1
  57. package/lib/common/llm-context.d.ts.map +1 -1
  58. package/lib/common/llm-context.js.map +1 -1
  59. package/lib/node/base-language-model.d.ts +1 -1
  60. package/lib/node/base-language-model.d.ts.map +1 -1
  61. package/lib/node/base-language-model.js +15 -4
  62. package/lib/node/base-language-model.js.map +1 -1
  63. package/package.json +23 -23
  64. package/src/browser/chat/chat-manager.service.ts +3 -2
  65. package/src/browser/chat/chat-model.ts +10 -2
  66. package/src/browser/chat/chat-proxy.service.ts +1 -0
  67. package/src/browser/chat/chat.feature.registry.ts +10 -1
  68. package/src/browser/chat/chat.internal.service.ts +2 -2
  69. package/src/browser/chat/chat.view.tsx +18 -8
  70. package/src/browser/components/ChatEditor.tsx +8 -0
  71. package/src/browser/components/ChatInput.tsx +2 -2
  72. package/src/browser/components/ChatMentionInput.tsx +100 -6
  73. package/src/browser/components/components.module.less +49 -0
  74. package/src/browser/components/mention-input/mention-input.tsx +145 -64
  75. package/src/browser/components/mention-input/types.ts +3 -1
  76. package/src/browser/context/llm-context.service.ts +2 -0
  77. package/src/browser/mcp/base-apply.service.ts +7 -6
  78. package/src/browser/mcp/config/components/mcp-config.view.tsx +3 -3
  79. package/src/browser/model/msg-history-manager.ts +1 -1
  80. package/src/browser/types.ts +8 -1
  81. package/src/common/index.ts +6 -0
  82. package/src/common/llm-context.ts +2 -0
  83. package/src/node/base-language-model.ts +25 -3
@@ -281,7 +281,7 @@ export const AIChatView = () => {
281
281
  if (loading) {
282
282
  return;
283
283
  }
284
- await handleSend(message.message, message.agentId, message.command);
284
+ await handleSend(message.message, message.images, message.agentId, message.command);
285
285
  } else {
286
286
  if (message.agentId) {
287
287
  setAgentId(message.agentId);
@@ -462,10 +462,16 @@ export const AIChatView = () => {
462
462
  );
463
463
 
464
464
  const renderUserMessage = React.useCallback(
465
- async (renderModel: { message: string; agentId?: string; relationId: string; command?: string }) => {
465
+ async (renderModel: {
466
+ message: string;
467
+ images?: string[];
468
+ agentId?: string;
469
+ relationId: string;
470
+ command?: string;
471
+ }) => {
466
472
  const ChatUserRoleRender = chatRenderRegistry.chatUserRoleRender;
467
473
 
468
- const { message, agentId, relationId, command } = renderModel;
474
+ const { message, images, agentId, relationId, command } = renderModel;
469
475
 
470
476
  const visibleAgentId = agentId === ChatProxyService.AGENT_ID ? '' : agentId;
471
477
 
@@ -474,12 +480,13 @@ export const AIChatView = () => {
474
480
  id: uuid(6),
475
481
  relationId,
476
482
  text: ChatUserRoleRender ? (
477
- <ChatUserRoleRender content={message} agentId={visibleAgentId} command={command} />
483
+ <ChatUserRoleRender content={message} images={images} agentId={visibleAgentId} command={command} />
478
484
  ) : (
479
485
  <CodeBlockWrapperInput
480
486
  labelService={labelService}
481
487
  relationId={relationId}
482
488
  text={message}
489
+ images={images}
483
490
  agentId={visibleAgentId}
484
491
  command={command}
485
492
  workspaceService={workspaceService}
@@ -598,10 +605,10 @@ export const AIChatView = () => {
598
605
 
599
606
  const handleAgentReply = React.useCallback(
600
607
  async (value: IChatMessageStructure) => {
601
- const { message, agentId, command, reportExtra } = value;
608
+ const { message, images, agentId, command, reportExtra } = value;
602
609
  const { actionType, actionSource } = reportExtra || {};
603
610
 
604
- const request = aiChatService.createRequest(message, agentId!, command);
611
+ const request = aiChatService.createRequest(message, agentId!, images, command);
605
612
  if (!request) {
606
613
  return;
607
614
  }
@@ -627,6 +634,7 @@ export const AIChatView = () => {
627
634
 
628
635
  msgHistoryManager.addUserMessage({
629
636
  content: message,
637
+ images: images || [],
630
638
  agentId: agentId!,
631
639
  agentCommand: command!,
632
640
  relationId,
@@ -635,6 +643,7 @@ export const AIChatView = () => {
635
643
  await renderUserMessage({
636
644
  relationId,
637
645
  message,
646
+ images,
638
647
  command,
639
648
  agentId,
640
649
  });
@@ -668,7 +677,7 @@ export const AIChatView = () => {
668
677
  );
669
678
 
670
679
  const handleSend = React.useCallback(
671
- async (message: string, agentId?: string, command?: string) => {
680
+ async (message: string, images?: string[], agentId?: string, command?: string) => {
672
681
  const reportExtra = {
673
682
  actionSource: ActionSourceEnum.Chat,
674
683
  actionType: ActionTypeEnum.Send,
@@ -707,7 +716,7 @@ export const AIChatView = () => {
707
716
  processedContent = processedContent.replace(match, `\`<attached_folder>${relativePath}\``);
708
717
  }
709
718
  }
710
- return handleAgentReply({ message: processedContent, agentId, command, reportExtra });
719
+ return handleAgentReply({ message: processedContent, images, agentId, command, reportExtra });
711
720
  },
712
721
  [handleAgentReply],
713
722
  );
@@ -750,6 +759,7 @@ export const AIChatView = () => {
750
759
  message: msg.content,
751
760
  agentId: msg.agentId,
752
761
  command: msg.agentCommand,
762
+ images: msg.images,
753
763
  });
754
764
  } else if (msg.role === ChatMessageRole.Assistant && msg.requestId) {
755
765
  const request = aiChatService.sessionModel.getRequest(msg.requestId)!;
@@ -2,6 +2,7 @@ import capitalize from 'lodash/capitalize';
2
2
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
3
3
  import Highlight from 'react-highlight';
4
4
 
5
+ import { Image } from '@opensumi/ide-components/lib/image';
5
6
  import {
6
7
  EDITOR_COMMANDS,
7
8
  FILE_COMMANDS,
@@ -336,6 +337,7 @@ export const CodeBlockWrapper = ({
336
337
 
337
338
  export const CodeBlockWrapperInput = ({
338
339
  text,
340
+ images,
339
341
  relationId,
340
342
  agentId,
341
343
  command,
@@ -344,6 +346,7 @@ export const CodeBlockWrapperInput = ({
344
346
  commandService,
345
347
  }: {
346
348
  text: string;
349
+ images?: string[];
347
350
  relationId: string;
348
351
  agentId?: string;
349
352
  command?: string;
@@ -370,6 +373,11 @@ export const CodeBlockWrapperInput = ({
370
373
 
371
374
  return (
372
375
  <div className={styles.ai_chat_code_wrapper}>
376
+ {images?.map((image) => (
377
+ <div className={styles.image_wrapper}>
378
+ <Image src={image} />
379
+ </div>
380
+ ))}
373
381
  <div className={styles.render_text}>
374
382
  {tag && (
375
383
  <div className={styles.tag_wrapper}>
@@ -162,7 +162,7 @@ const AgentWidget = ({ agentId, command }) => (
162
162
  );
163
163
 
164
164
  export interface IChatInputProps {
165
- onSend: (value: string, agentId?: string, command?: string) => void;
165
+ onSend: (value: string, images?: string[], agentId?: string, command?: string) => void;
166
166
  onValueChange?: (value: string) => void;
167
167
  onExpand?: (value: boolean) => void;
168
168
  placeholder?: string;
@@ -341,7 +341,7 @@ export const ChatInput = React.forwardRef((props: IChatInputProps, ref) => {
341
341
  }
342
342
 
343
343
  const handleSendLogic = (newValue: string = value) => {
344
- onSend(newValue, agentId, command);
344
+ onSend(newValue, [], agentId, command);
345
345
  setValue('');
346
346
  setTheme('');
347
347
  setAgentId('');
@@ -1,14 +1,18 @@
1
+ import { DataContent } from 'ai';
1
2
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
3
 
4
+ import { Image } from '@opensumi/ide-components/lib/image';
3
5
  import { LabelService, RecentFilesManager, useInjectable } from '@opensumi/ide-core-browser';
4
- import { getIcon } from '@opensumi/ide-core-browser/lib/components';
5
- import { URI, localize } from '@opensumi/ide-core-common';
6
+ import { Icon, getIcon } from '@opensumi/ide-core-browser/lib/components';
7
+ import { ChatFeatureRegistryToken, URI, localize } from '@opensumi/ide-core-common';
6
8
  import { CommandService } from '@opensumi/ide-core-common/lib/command';
7
9
  import { WorkbenchEditorService } from '@opensumi/ide-editor';
8
10
  import { FileSearchServicePath, IFileSearchService } from '@opensumi/ide-file-search';
11
+ import { IMessageService } from '@opensumi/ide-overlay';
9
12
  import { IWorkspaceService } from '@opensumi/ide-workspace';
10
13
 
11
14
  import { IChatInternalService } from '../../common';
15
+ import { ChatFeatureRegistry } from '../chat/chat.feature.registry';
12
16
  import { ChatInternalService } from '../chat/chat.internal.service';
13
17
  import { OPEN_MCP_CONFIG_COMMAND } from '../mcp/config/mcp-config.commands';
14
18
 
@@ -17,7 +21,13 @@ import { MentionInput } from './mention-input/mention-input';
17
21
  import { FooterButtonPosition, FooterConfig, MentionItem, MentionType } from './mention-input/types';
18
22
 
19
23
  export interface IChatMentionInputProps {
20
- onSend: (value: string, agentId?: string, command?: string, option?: { model: string; [key: string]: any }) => void;
24
+ onSend: (
25
+ value: string,
26
+ images?: string[],
27
+ agentId?: string,
28
+ command?: string,
29
+ option?: { model: string; [key: string]: any },
30
+ ) => void;
21
31
  onValueChange?: (value: string) => void;
22
32
  onExpand?: (value: boolean) => void;
23
33
  placeholder?: string;
@@ -26,6 +36,7 @@ export interface IChatMentionInputProps {
26
36
  sendBtnClassName?: string;
27
37
  defaultHeight?: number;
28
38
  value?: string;
39
+ images?: Array<DataContent | URL>;
29
40
  autoFocus?: boolean;
30
41
  theme?: string | null;
31
42
  setTheme: (theme: string | null) => void;
@@ -41,6 +52,7 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
41
52
  const { onSend, disabled = false } = props;
42
53
 
43
54
  const [value, setValue] = useState(props.value || '');
55
+ const [images, setImages] = useState(props.images || []);
44
56
  const aiChatService = useInjectable<ChatInternalService>(IChatInternalService);
45
57
  const commandService = useInjectable<CommandService>(CommandService);
46
58
  const searchService = useInjectable<IFileSearchService>(FileSearchServicePath);
@@ -48,7 +60,8 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
48
60
  const workspaceService = useInjectable<IWorkspaceService>(IWorkspaceService);
49
61
  const editorService = useInjectable<WorkbenchEditorService>(WorkbenchEditorService);
50
62
  const labelService = useInjectable<LabelService>(LabelService);
51
-
63
+ const messageService = useInjectable<IMessageService>(IMessageService);
64
+ const chatFeatureRegistry = useInjectable<ChatFeatureRegistry>(ChatFeatureRegistryToken);
52
65
  const handleShowMCPConfig = React.useCallback(() => {
53
66
  commandService.executeCommand(OPEN_MCP_CONFIG_COMMAND.id);
54
67
  }, [commandService]);
@@ -239,6 +252,24 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
239
252
  onClick: handleShowMCPConfig,
240
253
  position: FooterButtonPosition.LEFT,
241
254
  },
255
+ {
256
+ id: 'upload-image',
257
+ icon: 'image',
258
+ title: localize('aiNative.chat.imageUpload'),
259
+ onClick: () => {
260
+ const input = document.createElement('input');
261
+ input.type = 'file';
262
+ input.accept = 'image/*';
263
+ input.onchange = (e) => {
264
+ const files = (e.target as HTMLInputElement).files;
265
+ if (files?.length) {
266
+ handleImageUpload(Array.from(files));
267
+ }
268
+ };
269
+ input.click();
270
+ },
271
+ position: FooterButtonPosition.LEFT,
272
+ },
242
273
  ],
243
274
  showModelSelector: true,
244
275
  }),
@@ -254,13 +285,54 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
254
285
  if (disabled) {
255
286
  return;
256
287
  }
257
- onSend(content, undefined, undefined, option);
288
+ onSend(
289
+ content,
290
+ images.map((image) => image.toString()),
291
+ undefined,
292
+ undefined,
293
+ option,
294
+ );
295
+ setImages(props.images || []);
258
296
  },
259
- [onSend, editorService, disabled],
297
+ [onSend, images, disabled],
298
+ );
299
+
300
+ const handleImageUpload = useCallback(
301
+ async (files: File[]) => {
302
+ const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
303
+
304
+ // Validate file types
305
+ const invalidFiles = files.filter((file) => !allowedTypes.includes(file.type));
306
+ if (invalidFiles.length > 0) {
307
+ messageService.error('Only JPG, PNG, WebP and GIF images are supported');
308
+ return;
309
+ }
310
+
311
+ const imageUploadProvider = chatFeatureRegistry.getImageUploadProvider();
312
+ if (!imageUploadProvider) {
313
+ messageService.error('No image upload provider found');
314
+ return;
315
+ }
316
+
317
+ // Upload all files
318
+ const uploadedData = await Promise.all(files.map((file) => imageUploadProvider.imageUpload(file)));
319
+
320
+ const newImages = [...images, ...uploadedData];
321
+ setImages(newImages);
322
+ },
323
+ [images],
324
+ );
325
+
326
+ const handleDeleteImage = useCallback(
327
+ (index: number) => {
328
+ setImages(images.filter((_, i) => i !== index));
329
+ },
330
+ [images],
260
331
  );
261
332
 
262
333
  return (
263
334
  <div className={styles.chat_input_container}>
335
+ {images.length > 0 && <ImagePreviewer images={images} onDelete={handleDeleteImage} />}
264
336
  <MentionInput
265
337
  mentionItems={defaultMenuItems}
266
338
  onSend={handleSend}
@@ -270,7 +342,29 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
270
342
  workspaceService={workspaceService}
271
343
  placeholder={localize('aiNative.chat.input.placeholder.default')}
272
344
  footerConfig={defaultMentionInputFooterOptions}
345
+ onImageUpload={handleImageUpload}
273
346
  />
274
347
  </div>
275
348
  );
276
349
  };
350
+
351
+ const ImagePreviewer = ({
352
+ images,
353
+ onDelete,
354
+ }: {
355
+ images: Array<DataContent | URL>;
356
+ onDelete: (index: number) => void;
357
+ }) => (
358
+ <div>
359
+ <div className={styles.thumbnail_container}>
360
+ {images.map((image, index) => (
361
+ <div key={index} className={styles.thumbnail}>
362
+ <Image src={image.toString()} />
363
+ <button onClick={() => onDelete(index)} className={styles.delete_button}>
364
+ <Icon iconClass='codicon codicon-close' />
365
+ </button>
366
+ </div>
367
+ ))}
368
+ </div>
369
+ </div>
370
+ );
@@ -601,3 +601,52 @@
601
601
  vertical-align: middle;
602
602
  font-size: 12px;
603
603
  }
604
+
605
+ .thumbnail_container {
606
+ display: flex;
607
+ gap: 4px;
608
+ padding: 4px 12px;
609
+ .thumbnail {
610
+ width: 36px;
611
+ height: 36px;
612
+ border-radius: 4px;
613
+ position: relative;
614
+ box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.3);
615
+ padding: 2px;
616
+ display: inline-flex;
617
+ align-items: center;
618
+ img {
619
+ width: 100%;
620
+ height: 100%;
621
+ object-fit: cover;
622
+ }
623
+ }
624
+ .delete_button {
625
+ position: absolute;
626
+ top: -5px;
627
+ right: -5px;
628
+ font-size: 12px;
629
+ padding: 2px;
630
+ border: 0;
631
+ border-radius: 50%;
632
+ transition: transform 0.2s ease-in-out;
633
+ width: 16px;
634
+ height: 16px;
635
+ display: flex;
636
+ justify-content: center;
637
+ align-items: center;
638
+ background-color: var(--badge-background);
639
+ &:hover {
640
+ cursor: pointer;
641
+ transform: scale(1.2);
642
+ }
643
+ :global(.codicon) {
644
+ font-size: 12px;
645
+ color: var(--badge-foreground);
646
+ }
647
+ }
648
+ }
649
+
650
+ .image_wrapper {
651
+ display: inline-block;
652
+ }
@@ -18,6 +18,7 @@ export const MentionInput: React.FC<MentionInputProps> = ({
18
18
  loading = false,
19
19
  mentionKeyword = MENTION_KEYWORD,
20
20
  onSelectionChange,
21
+ onImageUpload,
21
22
  labelService,
22
23
  workspaceService,
23
24
  placeholder = 'Ask anything, @ to mention',
@@ -318,14 +319,74 @@ export const MentionInput: React.FC<MentionInputProps> = ({
318
319
  inlineSearchStartPos: null,
319
320
  loading: false,
320
321
  });
322
+ }
323
+
324
+ // 处理上下方向键导航历史记录
325
+ if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && !e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey) {
326
+ // 只有在非提及面板激活状态下才处理历史导航
327
+ if (!mentionState.active && !mentionState.inlineSearchActive && editorRef.current && history.length > 0) {
328
+ const currentContent = editorRef.current.innerHTML;
329
+
330
+ // 检查是否应该触发历史导航
331
+ const shouldTriggerHistory =
332
+ // 当前内容为空
333
+ !currentContent ||
334
+ currentContent === '<br>' ||
335
+ // 或者当前内容与历史记录中的某一项匹配(正在浏览历史)
336
+ (isNavigatingHistory && historyIndex >= 0 && history[history.length - 1 - historyIndex] === currentContent);
337
+
338
+ if (shouldTriggerHistory) {
339
+ e.preventDefault();
340
+
341
+ // 如果是第一次按上下键,保存当前输入
342
+ if (!isNavigatingHistory) {
343
+ setCurrentInput(currentContent);
344
+ setIsNavigatingHistory(true);
345
+ }
346
+
347
+ // 计算新的历史索引
348
+ let newIndex = historyIndex;
349
+ if (e.key === 'ArrowUp') {
350
+ // 向上导航到较早的历史记录
351
+ newIndex = Math.min(history.length - 1, historyIndex + 1);
352
+ } else {
353
+ // 向下导航到较新的历史记录
354
+ newIndex = Math.max(-1, historyIndex - 1);
355
+ }
356
+
357
+ setHistoryIndex(newIndex);
358
+
359
+ // 更新编辑器内容
360
+ if (newIndex === -1) {
361
+ // 恢复到当前输入
362
+ editorRef.current.innerHTML = currentInput;
363
+ } else {
364
+ // 显示历史记录
365
+ editorRef.current.innerHTML = history[history.length - 1 - newIndex];
366
+ }
321
367
 
322
- // 不要阻止默认行为,让 @ 正常输入到编辑器中
368
+ // 将光标移到末尾
369
+ const range = document.createRange();
370
+ range.selectNodeContents(editorRef.current);
371
+ range.collapse(false);
372
+ const selection = window.getSelection();
373
+ if (selection) {
374
+ selection.removeAllRanges();
375
+ selection.addRange(range);
376
+ }
377
+
378
+ return;
379
+ }
380
+ }
381
+ } else if (isNavigatingHistory && e.key !== 'ArrowUp' && e.key !== 'ArrowDown') {
382
+ // 如果用户在浏览历史记录后开始输入其他内容,退出历史导航模式
383
+ setIsNavigatingHistory(false);
384
+ setHistoryIndex(-1);
323
385
  }
324
386
 
325
387
  // 添加对 Enter 键的处理,只有在按下 Shift+Enter 时才允许换行
326
388
  if (e.key === 'Enter') {
327
389
  // 检查是否是输入法的回车键
328
- // isComposing 属性表示是否正在进行输入法组合输入
329
390
  if (e.nativeEvent.isComposing) {
330
391
  return; // 如果是输入法组合输入过程中的回车,不做任何处理
331
392
  }
@@ -339,57 +400,6 @@ export const MentionInput: React.FC<MentionInputProps> = ({
339
400
  }
340
401
  }
341
402
 
342
- // 处理上下方向键导航历史记录
343
- if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
344
- // 只有在非提及面板激活状态下才处理历史导航
345
- if (!mentionState.active && !mentionState.inlineSearchActive && editorRef.current) {
346
- e.preventDefault();
347
-
348
- // 如果是第一次按上下键,保存当前输入
349
- if (!isNavigatingHistory) {
350
- setCurrentInput(editorRef.current.innerHTML);
351
- setIsNavigatingHistory(true);
352
- }
353
-
354
- // 计算新的历史索引
355
- let newIndex = historyIndex;
356
- if (e.key === 'ArrowUp') {
357
- // 向上导航到较早的历史记录
358
- newIndex = Math.min(history.length - 1, historyIndex + 1);
359
- } else {
360
- // 向下导航到较新的历史记录
361
- newIndex = Math.max(-1, historyIndex - 1);
362
- }
363
-
364
- setHistoryIndex(newIndex);
365
-
366
- // 更新编辑器内容
367
- if (newIndex === -1) {
368
- // 恢复到当前输入
369
- editorRef.current.innerHTML = currentInput;
370
- } else {
371
- // 显示历史记录
372
- editorRef.current.innerHTML = history[history.length - 1 - newIndex];
373
- }
374
-
375
- // 将光标移到末尾
376
- const range = document.createRange();
377
- range.selectNodeContents(editorRef.current);
378
- range.collapse(false);
379
- const selection = window.getSelection();
380
- if (selection) {
381
- selection.removeAllRanges();
382
- selection.addRange(range);
383
- }
384
-
385
- return;
386
- }
387
- } else if (isNavigatingHistory && e.key !== 'ArrowUp' && e.key !== 'ArrowDown') {
388
- // 如果用户在浏览历史记录后开始输入,退出历史导航模式
389
- setIsNavigatingHistory(false);
390
- setHistoryIndex(-1);
391
- }
392
-
393
403
  // 如果提及面板未激活,不处理其他键盘事件
394
404
  if (!mentionState.active) {
395
405
  return;
@@ -404,8 +414,6 @@ export const MentionInput: React.FC<MentionInputProps> = ({
404
414
  filteredItems = filteredItems.filter((item) => item.text.toLowerCase().includes(searchText));
405
415
  }
406
416
 
407
- // 二级菜单过滤已经在 getCurrentItems 中处理
408
-
409
417
  if (filteredItems.length === 0) {
410
418
  return;
411
419
  }
@@ -431,14 +439,6 @@ export const MentionInput: React.FC<MentionInputProps> = ({
431
439
  e.preventDefault();
432
440
  }
433
441
  }
434
-
435
- // 处理 Backspace 键,检查是否需要清空编辑器
436
- if (e.key === 'Backspace' && editorRef.current) {
437
- const content = editorRef.current.innerHTML;
438
- if (content === '<br>' || content === '<br/>') {
439
- editorRef.current.innerHTML = '';
440
- }
441
- }
442
442
  };
443
443
 
444
444
  // 添加对输入法事件的处理
@@ -447,6 +447,86 @@ export const MentionInput: React.FC<MentionInputProps> = ({
447
447
  // 这里可以添加额外的逻辑,如果需要的话
448
448
  };
449
449
 
450
+ // 添加粘贴事件处理
451
+ const handlePaste = async (e: React.ClipboardEvent<HTMLDivElement>) => {
452
+ const items = e.clipboardData.items;
453
+
454
+ // 先收集所有图片文件
455
+ const imageFiles: File[] = [];
456
+ // eslint-disable-next-line @typescript-eslint/prefer-for-of
457
+ for (let i = 0; i < items.length; i++) {
458
+ if (items[i].kind === 'file' && items[i].type.startsWith('image/')) {
459
+ const file = items[i].getAsFile();
460
+ if (file) {
461
+ imageFiles.push(file);
462
+ }
463
+ }
464
+ }
465
+
466
+ e.preventDefault();
467
+
468
+ // 处理所有收集到的图片
469
+ if (imageFiles.length > 0 && onImageUpload) {
470
+ await onImageUpload(imageFiles);
471
+ return;
472
+ }
473
+
474
+ const text = e.clipboardData.getData('text/plain');
475
+
476
+ // 处理文本,保留换行和缩进
477
+ const processedText = text
478
+ .replace(/\t/g, ' ')
479
+ .replace(/\n\s*\n/g, '\n\n')
480
+ .replace(/[ \t]+$/gm, '');
481
+
482
+ const selection = window.getSelection();
483
+ if (!selection || !selection.rangeCount) {
484
+ return;
485
+ }
486
+
487
+ const range = selection.getRangeAt(0);
488
+ range.deleteContents();
489
+
490
+ // 将处理后的文本按行分割
491
+ const lines = processedText.split('\n');
492
+ const fragment = document.createDocumentFragment();
493
+
494
+ lines.forEach((line, index) => {
495
+ // 处理行首空格,将每个空格转换为 &nbsp;
496
+ const processedLine = line.replace(/^[ ]+/g, (match) => {
497
+ const span = document.createElement('span');
498
+ span.innerHTML = '\u00A0'.repeat(match.length);
499
+ return span.innerHTML;
500
+ });
501
+
502
+ // 创建一个临时容器来保持 HTML 内容
503
+ const container = document.createElement('span');
504
+ container.innerHTML = processedLine;
505
+
506
+ // 将容器的内容添加到文档片段
507
+ while (container.firstChild) {
508
+ fragment.appendChild(container.firstChild);
509
+ }
510
+
511
+ // 如果不是最后一行,添加换行符
512
+ if (index < lines.length - 1) {
513
+ fragment.appendChild(document.createElement('br'));
514
+ }
515
+ });
516
+
517
+ // 插入处理后的内容
518
+ range.insertNode(fragment);
519
+
520
+ // 将光标移动到插入内容的末尾
521
+ range.setStartAfter(fragment);
522
+ range.setEndAfter(fragment);
523
+ selection.removeAllRanges();
524
+ selection.addRange(range);
525
+
526
+ // 触发 input 事件以更新状态
527
+ handleInput();
528
+ };
529
+
450
530
  // 初始化编辑器
451
531
  React.useEffect(() => {
452
532
  if (editorRef.current) {
@@ -867,7 +947,7 @@ export const MentionInput: React.FC<MentionInputProps> = ({
867
947
  title={button.title}
868
948
  >
869
949
  <EnhanceIcon
870
- className={cls(getIcon(button.icon), styles[`${button.id}_logo`])}
950
+ className={cls(button.icon ? getIcon(button.icon) : button.iconClass, styles[`${button.id}_logo`])}
871
951
  tabIndex={0}
872
952
  role='button'
873
953
  ariaLabel={button.title}
@@ -901,6 +981,7 @@ export const MentionInput: React.FC<MentionInputProps> = ({
901
981
  contentEditable={true}
902
982
  onInput={handleInput}
903
983
  onKeyDown={handleKeyDown}
984
+ onPaste={handlePaste}
904
985
  onCompositionEnd={handleCompositionEnd}
905
986
  />
906
987
  </div>
@@ -55,7 +55,8 @@ export enum MentionType {
55
55
 
56
56
  interface FooterButton {
57
57
  id: string;
58
- icon: string;
58
+ icon?: string;
59
+ iconClass?: string;
59
60
  title: string;
60
61
  onClick?: () => void;
61
62
  position: FooterButtonPosition;
@@ -75,6 +76,7 @@ export interface MentionInputProps {
75
76
  placeholder?: string;
76
77
  loading?: boolean;
77
78
  onSelectionChange?: (value: string) => void;
79
+ onImageUpload?: (files: File[]) => Promise<void>;
78
80
  footerConfig?: FooterConfig; // 新增配置项
79
81
  mentionKeyword?: string;
80
82
  labelService?: LabelService;
@@ -1,3 +1,5 @@
1
+ import { DataContent } from 'ai';
2
+
1
3
  import { Autowired, Injectable } from '@opensumi/di';
2
4
  import { AppConfig } from '@opensumi/ide-core-browser/lib/react-providers/config-provider';
3
5
  import { WithEventBus } from '@opensumi/ide-core-common/lib/event-bus/event-decorator';