Repository: opensumi/core
Pull request #4476: feat: support add image to chat
Author: login: ensorrow
Labels: name: 🎨 feature
description: feature required
Created: 2025-03-13T14:00:59Z | Merged: 2025-03-17T02:59:36Z

### Types

- [x] 🎉 New Features

### Background or solution
<img width="483" alt="image" src="https://github.com/user-attachments/assets/dd9d9f0d-2acf-4fee-8c17-4eb4c253a37c" />

### Changelog
feat: support add image to chat

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **新增功能**
  - 聊天功能现已支持图片上传、预览与删除，用户可在消息中附加图片，提升交流体验。

- **样式更新**
  - 改进了缩略图、预览及操作按钮的视觉效果，为图片展示和交互带来更流畅的体验。

- **依赖更新**
  - 新增图像处理相关依赖，确保图片渲染和预览功能的稳定性。
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/packages/ai-native/src/browser/chat/chat-manager.service.ts b/packages/ai-native/src/browser/chat/chat-manager.service.ts
index 65258dcb5d..f7ee31cd24 100644
--- a/packages/ai-native/src/browser/chat/chat-manager.service.ts
+++ b/packages/ai-native/src/browser/chat/chat-manager.service.ts
@@ -150,7 +150,7 @@ export class ChatManagerService extends Disposable {
     this.saveSessions();
   }
 
-  createRequest(sessionId: string, message: string, agentId: string, command?: string) {
+  createRequest(sessionId: string, message: string, agentId: string, command?: string, images?: string[]) {
     const model = this.getSession(sessionId);
     if (!model) {
       throw new Error(`Unknown session: ${sessionId}`);
@@ -160,7 +160,7 @@ export class ChatManagerService extends Disposable {
       return;
     }
 
-    return model.addRequest({ prompt: message, agentId, command });
+    return model.addRequest({ prompt: message, agentId, command, images });
   }
 
   async sendRequest(sessionId: string, request: ChatRequestModel, regenerate: boolean) {
@@ -191,6 +191,7 @@ export class ChatManagerService extends Disposable {
         requestId: request.requestId,
         message: request.message.prompt,
         command: request.message.command,
+        images: request.message.images,
         regenerate,
       };
       const result = await this.chatAgentService.invokeAgent(
diff --git a/packages/ai-native/src/browser/chat/chat-model.ts b/packages/ai-native/src/browser/chat/chat-model.ts
index 2b85fd8ba8..6f87369df0 100644
--- a/packages/ai-native/src/browser/chat/chat-model.ts
+++ b/packages/ai-native/src/browser/chat/chat-model.ts
@@ -27,7 +27,7 @@ import {
 import { MsgHistoryManager } from '../model/msg-history-manager';
 import { IChatSlashCommandItem } from '../types';
 
-import type { TextPart, ToolCallPart } from 'ai';
+import type { ImagePart, TextPart, ToolCallPart } from 'ai';
 
 export type IChatProgressResponseContent =
   | IChatMarkdownContent
@@ -330,7 +330,15 @@ export class ChatModel extends Disposable implements IChatModel {
       if (!request.response.isComplete) {
         continue;
       }
-      history.push({ role: 'user', content: request.message.prompt });
+      history.push({
+        role: 'user',
+        content: request.message.images?.length
+          ? [
+              { type: 'text', text: request.message.prompt },
+              ...request.message.images.map((image) => ({ type: 'image', image: new URL(image) } as ImagePart)),
+            ]
+          : request.message.prompt,
+      });
       for (const part of request.response.responseParts) {
         if (part.kind === 'treeData' || part.kind === 'component') {
           continue;
diff --git a/packages/ai-native/src/browser/chat/chat-proxy.service.ts b/packages/ai-native/src/browser/chat/chat-proxy.service.ts
index bc50d6a9a0..307266c8bb 100644
--- a/packages/ai-native/src/browser/chat/chat-proxy.service.ts
+++ b/packages/ai-native/src/browser/chat/chat-proxy.service.ts
@@ -151,6 +151,7 @@ export class ChatProxyService extends Disposable {
               requestId: request.requestId,
               sessionId: request.sessionId,
               history,
+              images: request.images,
               ...this.getRequestOptions(),
             },
             token,
diff --git a/packages/ai-native/src/browser/chat/chat.feature.registry.ts b/packages/ai-native/src/browser/chat/chat.feature.registry.ts
index 070a4b2896..e3b57b52b6 100644
--- a/packages/ai-native/src/browser/chat/chat.feature.registry.ts
+++ b/packages/ai-native/src/browser/chat/chat.feature.registry.ts
@@ -2,7 +2,7 @@ import { Injectable } from '@opensumi/di';
 import { Disposable, Emitter, Event, getDebugLogger } from '@opensumi/ide-core-common';
 
 import { IChatWelcomeMessageContent, ISampleQuestions, SLASH_SYMBOL } from '../../common';
-import { IChatFeatureRegistry, IChatSlashCommandHandler, IChatSlashCommandItem } from '../types';
+import { IChatFeatureRegistry, IChatSlashCommandHandler, IChatSlashCommandItem, IImageUploadProvider } from '../types';
 
 import { ChatSlashCommandItemModel, ChatWelcomeMessageModel } from './chat-model';
 import { ChatProxyService } from './chat-proxy.service';
@@ -12,6 +12,15 @@ export class ChatFeatureRegistry extends Disposable implements IChatFeatureRegis
   private readonly logger = getDebugLogger();
   private slashCommandsMap: Map<string, ChatSlashCommandItemModel> = new Map();
   private slashCommandsHandlerMap: Map<string, IChatSlashCommandHandler> = new Map();
+  private imageUploadProvider: IImageUploadProvider | undefined;
+
+  public registerImageUploadProvider(provider: IImageUploadProvider): void {
+    this.imageUploadProvider = provider;
+  }
+
+  public getImageUploadProvider(): IImageUploadProvider | undefined {
+    return this.imageUploadProvider;
+  }
 
   public chatWelcomeMessageModel?: ChatWelcomeMessageModel;
 
diff --git a/packages/ai-native/src/browser/chat/chat.internal.service.ts b/packages/ai-native/src/browser/chat/chat.internal.service.ts
index bb14c9d948..d8196dea6c 100644
--- a/packages/ai-native/src/browser/chat/chat.internal.service.ts
+++ b/packages/ai-native/src/browser/chat/chat.internal.service.ts
@@ -62,8 +62,8 @@ export class ChatInternalService extends Disposable {
     this._onChangeRequestId.fire(id);
   }
 
-  createRequest(input: string, agentId: string, command?: string) {
-    return this.chatManagerService.createRequest(this.#sessionModel.sessionId, input, agentId, command);
+  createRequest(input: string, agentId: string, images?: string[], command?: string) {
+    return this.chatManagerService.createRequest(this.#sessionModel.sessionId, input, agentId, command, images);
   }
 
   sendRequest(request: ChatRequestModel, regenerate = false) {
diff --git a/packages/ai-native/src/browser/chat/chat.view.tsx b/packages/ai-native/src/browser/chat/chat.view.tsx
index 462bb11a3e..1f701915bf 100644
--- a/packages/ai-native/src/browser/chat/chat.view.tsx
+++ b/packages/ai-native/src/browser/chat/chat.view.tsx
@@ -281,7 +281,7 @@ export const AIChatView = () => {
           if (loading) {
             return;
           }
-          await handleSend(message.message, message.agentId, message.command);
+          await handleSend(message.message, message.images, message.agentId, message.command);
         } else {
           if (message.agentId) {
             setAgentId(message.agentId);
@@ -462,10 +462,16 @@ export const AIChatView = () => {
   );
 
   const renderUserMessage = React.useCallback(
-    async (renderModel: { message: string; agentId?: string; relationId: string; command?: string }) => {
+    async (renderModel: {
+      message: string;
+      images?: string[];
+      agentId?: string;
+      relationId: string;
+      command?: string;
+    }) => {
       const ChatUserRoleRender = chatRenderRegistry.chatUserRoleRender;
 
-      const { message, agentId, relationId, command } = renderModel;
+      const { message, images, agentId, relationId, command } = renderModel;
 
       const visibleAgentId = agentId === ChatProxyService.AGENT_ID ? '' : agentId;
 
@@ -474,12 +480,13 @@ export const AIChatView = () => {
           id: uuid(6),
           relationId,
           text: ChatUserRoleRender ? (
-            <ChatUserRoleRender content={message} agentId={visibleAgentId} command={command} />
+            <ChatUserRoleRender content={message} images={images} agentId={visibleAgentId} command={command} />
           ) : (
             <CodeBlockWrapperInput
               labelService={labelService}
               relationId={relationId}
               text={message}
+              images={images}
               agentId={visibleAgentId}
               command={command}
               workspaceService={workspaceService}
@@ -598,10 +605,10 @@ export const AIChatView = () => {
 
   const handleAgentReply = React.useCallback(
     async (value: IChatMessageStructure) => {
-      const { message, agentId, command, reportExtra } = value;
+      const { message, images, agentId, command, reportExtra } = value;
       const { actionType, actionSource } = reportExtra || {};
 
-      const request = aiChatService.createRequest(message, agentId!, command);
+      const request = aiChatService.createRequest(message, agentId!, images, command);
       if (!request) {
         return;
       }
@@ -627,6 +634,7 @@ export const AIChatView = () => {
 
       msgHistoryManager.addUserMessage({
         content: message,
+        images: images || [],
         agentId: agentId!,
         agentCommand: command!,
         relationId,
@@ -635,6 +643,7 @@ export const AIChatView = () => {
       await renderUserMessage({
         relationId,
         message,
+        images,
         command,
         agentId,
       });
@@ -668,7 +677,7 @@ export const AIChatView = () => {
   );
 
   const handleSend = React.useCallback(
-    async (message: string, agentId?: string, command?: string) => {
+    async (message: string, images?: string[], agentId?: string, command?: string) => {
       const reportExtra = {
         actionSource: ActionSourceEnum.Chat,
         actionType: ActionTypeEnum.Send,
@@ -707,7 +716,7 @@ export const AIChatView = () => {
           processedContent = processedContent.replace(match, `\`<attached_folder>${relativePath}\``);
         }
       }
-      return handleAgentReply({ message: processedContent, agentId, command, reportExtra });
+      return handleAgentReply({ message: processedContent, images, agentId, command, reportExtra });
     },
     [handleAgentReply],
   );
@@ -750,6 +759,7 @@ export const AIChatView = () => {
             message: msg.content,
             agentId: msg.agentId,
             command: msg.agentCommand,
+            images: msg.images,
           });
         } else if (msg.role === ChatMessageRole.Assistant && msg.requestId) {
           const request = aiChatService.sessionModel.getRequest(msg.requestId)!;
diff --git a/packages/ai-native/src/browser/components/ChatEditor.tsx b/packages/ai-native/src/browser/components/ChatEditor.tsx
index 4035b66e97..a4e2c7682f 100644
--- a/packages/ai-native/src/browser/components/ChatEditor.tsx
+++ b/packages/ai-native/src/browser/components/ChatEditor.tsx
@@ -2,6 +2,7 @@ import capitalize from 'lodash/capitalize';
 import React, { useCallback, useEffect, useMemo, useState } from 'react';
 import Highlight from 'react-highlight';
 
+import { Image } from '@opensumi/ide-components/lib/image';
 import {
   EDITOR_COMMANDS,
   FILE_COMMANDS,
@@ -336,6 +337,7 @@ export const CodeBlockWrapper = ({
 
 export const CodeBlockWrapperInput = ({
   text,
+  images,
   relationId,
   agentId,
   command,
@@ -344,6 +346,7 @@ export const CodeBlockWrapperInput = ({
   commandService,
 }: {
   text: string;
+  images?: string[];
   relationId: string;
   agentId?: string;
   command?: string;
@@ -370,6 +373,11 @@ export const CodeBlockWrapperInput = ({
 
   return (
     <div className={styles.ai_chat_code_wrapper}>
+      {images?.map((image) => (
+        <div className={styles.image_wrapper}>
+          <Image src={image} />
+        </div>
+      ))}
       <div className={styles.render_text}>
         {tag && (
           <div className={styles.tag_wrapper}>
diff --git a/packages/ai-native/src/browser/components/ChatInput.tsx b/packages/ai-native/src/browser/components/ChatInput.tsx
index 7490ae3d0a..fbae384446 100644
--- a/packages/ai-native/src/browser/components/ChatInput.tsx
+++ b/packages/ai-native/src/browser/components/ChatInput.tsx
@@ -162,7 +162,7 @@ const AgentWidget = ({ agentId, command }) => (
 );
 
 export interface IChatInputProps {
-  onSend: (value: string, agentId?: string, command?: string) => void;
+  onSend: (value: string, images?: string[], agentId?: string, command?: string) => void;
   onValueChange?: (value: string) => void;
   onExpand?: (value: boolean) => void;
   placeholder?: string;
@@ -341,7 +341,7 @@ export const ChatInput = React.forwardRef((props: IChatInputProps, ref) => {
     }
 
     const handleSendLogic = (newValue: string = value) => {
-      onSend(newValue, agentId, command);
+      onSend(newValue, [], agentId, command);
       setValue('');
       setTheme('');
       setAgentId('');
diff --git a/packages/ai-native/src/browser/components/ChatMentionInput.tsx b/packages/ai-native/src/browser/components/ChatMentionInput.tsx
index 43e4720a9a..3330000478 100644
--- a/packages/ai-native/src/browser/components/ChatMentionInput.tsx
+++ b/packages/ai-native/src/browser/components/ChatMentionInput.tsx
@@ -1,14 +1,18 @@
+import { DataContent } from 'ai';
 import React, { useCallback, useEffect, useMemo, useState } from 'react';
 
+import { Image } from '@opensumi/ide-components/lib/image';
 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 { 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 { WorkbenchEditorService } from '@opensumi/ide-editor';
 import { FileSearchServicePath, IFileSearchService } from '@opensumi/ide-file-search';
+import { IMessageService } from '@opensumi/ide-overlay';
 import { IWorkspaceService } from '@opensumi/ide-workspace';
 
 import { IChatInternalService } from '../../common';
+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';
 
@@ -17,7 +21,13 @@ 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;
+  onSend: (
+    value: string,
+    images?: string[],
+    agentId?: string,
+    command?: string,
+    option?: { model: string; [key: string]: any },
+  ) => void;
   onValueChange?: (value: string) => void;
   onExpand?: (value: boolean) => void;
   placeholder?: string;
@@ -26,6 +36,7 @@ export interface IChatMentionInputProps {
   sendBtnClassName?: string;
   defaultHeight?: number;
   value?: string;
+  images?: Array<DataContent | URL>;
   autoFocus?: boolean;
   theme?: string | null;
   setTheme: (theme: string | null) => void;
@@ -41,6 +52,7 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
   const { onSend, disabled = false } = props;
 
   const [value, setValue] = useState(props.value || '');
+  const [images, setImages] = useState(props.images || []);
   const aiChatService = useInjectable<ChatInternalService>(IChatInternalService);
   const commandService = useInjectable<CommandService>(CommandService);
   const searchService = useInjectable<IFileSearchService>(FileSearchServicePath);
@@ -48,7 +60,8 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
   const workspaceService = useInjectable<IWorkspaceService>(IWorkspaceService);
   const editorService = useInjectable<WorkbenchEditorService>(WorkbenchEditorService);
   const labelService = useInjectable<LabelService>(LabelService);
-
+  const messageService = useInjectable<IMessageService>(IMessageService);
+  const chatFeatureRegistry = useInjectable<ChatFeatureRegistry>(ChatFeatureRegistryToken);
   const handleShowMCPConfig = React.useCallback(() => {
     commandService.executeCommand(OPEN_MCP_CONFIG_COMMAND.id);
   }, [commandService]);
@@ -239,6 +252,24 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
           onClick: handleShowMCPConfig,
           position: FooterButtonPosition.LEFT,
         },
+        {
+          id: 'upload-image',
+          iconClass: 'codicon codicon-file-media',
+          title: 'Upload Image',
+          onClick: () => {
+            const input = document.createElement('input');
+            input.type = 'file';
+            input.accept = 'image/*';
+            input.onchange = (e) => {
+              const file = (e.target as HTMLInputElement).files?.[0];
+              if (file) {
+                handleImageUpload(file);
+              }
+            };
+            input.click();
+          },
+          position: FooterButtonPosition.LEFT,
+        },
       ],
       showModelSelector: true,
     }),
@@ -254,13 +285,47 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
       if (disabled) {
         return;
       }
-      onSend(content, undefined, undefined, option);
+      onSend(
+        content,
+        images.map((image) => image.toString()),
+        undefined,
+        undefined,
+        option,
+      );
+      setImages(props.images || []);
+    },
+    [onSend, images, disabled],
+  );
+
+  const handleImageUpload = useCallback(
+    async (file: File) => {
+      const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
+      if (!allowedTypes.includes(file.type)) {
+        messageService.error('Only JPG, PNG, WebP and GIF images are supported');
+        return;
+      }
+
+      const imageUploadProvider = chatFeatureRegistry.getImageUploadProvider();
+      if (!imageUploadProvider) {
+        messageService.error('No image upload provider found');
+        return;
+      }
+      const data = await imageUploadProvider.imageUpload(file);
+      setImages([...images, data]);
     },
-    [onSend, editorService, disabled],
+    [images],
+  );
+
+  const handleDeleteImage = useCallback(
+    (index: number) => {
+      setImages(images.filter((_, i) => i !== index));
+    },
+    [images],
   );
 
   return (
     <div className={styles.chat_input_container}>
+      {images.length > 0 && <ImagePreviewer images={images} onDelete={handleDeleteImage} />}
       <MentionInput
         mentionItems={defaultMenuItems}
         onSend={handleSend}
@@ -270,7 +335,29 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
         workspaceService={workspaceService}
         placeholder={localize('aiNative.chat.input.placeholder.default')}
         footerConfig={defaultMentionInputFooterOptions}
+        onImageUpload={handleImageUpload}
       />
     </div>
   );
 };
+
+const ImagePreviewer = ({
+  images,
+  onDelete,
+}: {
+  images: Array<DataContent | URL>;
+  onDelete: (index: number) => void;
+}) => (
+  <div>
+    <div className={styles.thumbnail_container}>
+      {images.map((image, index) => (
+        <div key={index} className={styles.thumbnail}>
+          <Image src={image.toString()} />
+          <button onClick={() => onDelete(index)} className={styles.delete_button}>
+            <Icon iconClass='codicon codicon-close' />
+          </button>
+        </div>
+      ))}
+    </div>
+  </div>
+);
diff --git a/packages/ai-native/src/browser/components/components.module.less b/packages/ai-native/src/browser/components/components.module.less
index 0f35ff7632..393f617cf8 100644
--- a/packages/ai-native/src/browser/components/components.module.less
+++ b/packages/ai-native/src/browser/components/components.module.less
@@ -601,3 +601,52 @@
   vertical-align: middle;
   font-size: 12px;
 }
+
+.thumbnail_container {
+  display: flex;
+  gap: 4px;
+  padding: 4px 12px;
+  .thumbnail {
+    width: 36px;
+    height: 36px;
+    border-radius: 4px;
+    position: relative;
+    box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.3);
+    padding: 2px;
+    display: inline-flex;
+    align-items: center;
+    img {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+  }
+  .delete_button {
+    position: absolute;
+    top: -5px;
+    right: -5px;
+    font-size: 12px;
+    padding: 2px;
+    border: 0;
+    border-radius: 50%;
+    transition: transform 0.2s ease-in-out;
+    width: 16px;
+    height: 16px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    background-color: var(--badge-background);
+    &:hover {
+      cursor: pointer;
+      transform: scale(1.2);
+    }
+    :global(.codicon) {
+      font-size: 12px;
+      color: var(--badge-foreground);
+    }
+  }
+}
+
+.image_wrapper {
+  display: inline-block;
+}
diff --git a/packages/ai-native/src/browser/components/mention-input/mention-input.tsx b/packages/ai-native/src/browser/components/mention-input/mention-input.tsx
index d162ebeeb9..e17358afd8 100644
--- a/packages/ai-native/src/browser/components/mention-input/mention-input.tsx
+++ b/packages/ai-native/src/browser/components/mention-input/mention-input.tsx
@@ -18,6 +18,7 @@ export const MentionInput: React.FC<MentionInputProps> = ({
   loading = false,
   mentionKeyword = MENTION_KEYWORD,
   onSelectionChange,
+  onImageUpload,
   labelService,
   workspaceService,
   placeholder = 'Ask anything, @ to mention',
@@ -275,6 +276,21 @@ export const MentionInput: React.FC<MentionInputProps> = ({
     }
   };
 
+  // 处理图片粘贴事件
+  const handlePaste = (e: React.ClipboardEvent<HTMLDivElement>) => {
+    const items = e.clipboardData.items;
+    // eslint-disable-next-line @typescript-eslint/prefer-for-of
+    for (let i = 0; i < items.length; i++) {
+      if (items[i].kind === 'file' && items[i].type.startsWith('image/')) {
+        const file = items[i].getAsFile();
+        if (file && onImageUpload) {
+          e.preventDefault();
+          onImageUpload(file);
+        }
+      }
+    }
+  };
+
   // 处理键盘事件
   const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
     // 如果按下ESC键且提及面板处于活动状态或内联搜索处于活动状态
@@ -867,7 +883,7 @@ export const MentionInput: React.FC<MentionInputProps> = ({
             title={button.title}
           >
             <EnhanceIcon
-              className={cls(getIcon(button.icon), styles[`${button.id}_logo`])}
+              className={cls(button.icon ? getIcon(button.icon) : button.iconClass, styles[`${button.id}_logo`])}
               tabIndex={0}
               role='button'
               ariaLabel={button.title}
@@ -901,6 +917,7 @@ export const MentionInput: React.FC<MentionInputProps> = ({
           contentEditable={true}
           onInput={handleInput}
           onKeyDown={handleKeyDown}
+          onPaste={handlePaste}
           onCompositionEnd={handleCompositionEnd}
         />
       </div>
diff --git a/packages/ai-native/src/browser/components/mention-input/types.ts b/packages/ai-native/src/browser/components/mention-input/types.ts
index ab576ff0d8..5524a4042f 100644
--- a/packages/ai-native/src/browser/components/mention-input/types.ts
+++ b/packages/ai-native/src/browser/components/mention-input/types.ts
@@ -55,7 +55,8 @@ export enum MentionType {
 
 interface FooterButton {
   id: string;
-  icon: string;
+  icon?: string;
+  iconClass?: string;
   title: string;
   onClick?: () => void;
   position: FooterButtonPosition;
@@ -75,6 +76,7 @@ export interface MentionInputProps {
   placeholder?: string;
   loading?: boolean;
   onSelectionChange?: (value: string) => void;
+  onImageUpload?: (file: File) => void;
   footerConfig?: FooterConfig; // 新增配置项
   mentionKeyword?: string;
   labelService?: LabelService;
diff --git a/packages/ai-native/src/browser/context/llm-context.service.ts b/packages/ai-native/src/browser/context/llm-context.service.ts
index 85e746da1d..b7cfc9fc85 100644
--- a/packages/ai-native/src/browser/context/llm-context.service.ts
+++ b/packages/ai-native/src/browser/context/llm-context.service.ts
@@ -1,3 +1,5 @@
+import { DataContent } from 'ai';
+
 import { Autowired, Injectable } from '@opensumi/di';
 import { AppConfig } from '@opensumi/ide-core-browser/lib/react-providers/config-provider';
 import { WithEventBus } from '@opensumi/ide-core-common/lib/event-bus/event-decorator';
diff --git a/packages/ai-native/src/browser/model/msg-history-manager.ts b/packages/ai-native/src/browser/model/msg-history-manager.ts
index 2ff5e366bf..99fa7636d9 100644
--- a/packages/ai-native/src/browser/model/msg-history-manager.ts
+++ b/packages/ai-native/src/browser/model/msg-history-manager.ts
@@ -62,7 +62,7 @@ export class MsgHistoryManager extends Disposable {
   }
 
   public addUserMessage(
-    message: Required<Pick<IExcludeMessage, 'agentId' | 'agentCommand' | 'content' | 'relationId'>>,
+    message: Required<Pick<IExcludeMessage, 'agentId' | 'agentCommand' | 'content' | 'relationId' | 'images'>>,
   ): string {
     return this.doAddMessage({
       ...message,
diff --git a/packages/ai-native/src/browser/types.ts b/packages/ai-native/src/browser/types.ts
index ffa1a4e7fe..e2691760e2 100644
--- a/packages/ai-native/src/browser/types.ts
+++ b/packages/ai-native/src/browser/types.ts
@@ -1,3 +1,4 @@
+import { DataContent } from 'ai';
 import React from 'react';
 import { ZodSchema } from 'zod';
 
@@ -129,6 +130,7 @@ export interface IChatSlashCommandHandler {
 }
 
 export interface IChatFeatureRegistry {
+  registerImageUploadProvider(provider: IImageUploadProvider): void;
   registerWelcome(content: IChatWelcomeMessageContent | React.ReactNode, sampleQuestions?: ISampleQuestions[]): void;
   registerSlashCommand(command: IChatSlashCommandItem, handler: IChatSlashCommandHandler): void;
 }
@@ -140,13 +142,14 @@ export type ChatWelcomeRender = (props: {
 export type ChatAIRoleRender = (props: { content: string }) => React.ReactElement | React.JSX.Element;
 export type ChatUserRoleRender = (props: {
   content: string;
+  images?: string[];
   agentId?: string;
   command?: string;
 }) => React.ReactElement | React.JSX.Element;
 export type ChatThinkingRender = (props: { thinkingText?: string }) => React.ReactElement | React.JSX.Element;
 export type ChatThinkingResultRender = (props: { thinkingResult?: string }) => React.ReactElement | React.JSX.Element;
 export type ChatInputRender = (props: {
-  onSend: (value: string, agentId?: string, command?: string) => void;
+  onSend: (value: string, images?: string[], agentId?: string, command?: string) => void;
   onValueChange?: (value: string) => void;
   onExpand?: (value: boolean) => void;
   placeholder?: string;
@@ -289,6 +292,10 @@ export interface IProblemFixProviderRegistry {
   registerHoverFixProvider(handler: IHoverFixHandler): void;
 }
 
+export interface IImageUploadProvider {
+  imageUpload(file: File): Promise<DataContent | URL>;
+}
+
 export const AINativeCoreContribution = Symbol('AINativeCoreContribution');
 
 export interface AINativeCoreContribution {
diff --git a/packages/ai-native/src/common/index.ts b/packages/ai-native/src/common/index.ts
index c231508da3..7d8c6e90f0 100644
--- a/packages/ai-native/src/common/index.ts
+++ b/packages/ai-native/src/common/index.ts
@@ -52,6 +52,10 @@ export interface IChatMessageStructure {
    * 用于 chat 面板展示
    */
   message: string;
+  /**
+   * 图片
+   */
+  images?: string[];
   /**
    * 实际调用的 prompt
    */
@@ -200,6 +204,7 @@ export interface IChatAgentRequest {
   requestId: string;
   command?: string;
   message: string;
+  images?: string[];
   regenerate?: boolean;
 }
 
@@ -237,6 +242,7 @@ export type IChatFollowup = IChatReplyFollowup | IChatResponseCommandFollowup;
 
 export interface IChatRequestMessage {
   prompt: string;
+  images?: string[];
   agentId: string;
   command?: string;
 }
diff --git a/packages/ai-native/src/common/llm-context.ts b/packages/ai-native/src/common/llm-context.ts
index 6e942ed0d2..00e6cfc730 100644
--- a/packages/ai-native/src/common/llm-context.ts
+++ b/packages/ai-native/src/common/llm-context.ts
@@ -1,3 +1,5 @@
+import { DataContent } from 'ai';
+
 import { Event, URI } from '@opensumi/ide-core-common/lib/utils';
 
 export interface LLMContextService {
diff --git a/packages/ai-native/src/node/base-language-model.ts b/packages/ai-native/src/node/base-language-model.ts
index d9cc95d5eb..cd4702e3e6 100644
--- a/packages/ai-native/src/node/base-language-model.ts
+++ b/packages/ai-native/src/node/base-language-model.ts
@@ -1,4 +1,13 @@
-import { CoreMessage, ToolExecutionOptions, jsonSchema, streamText, tool } from 'ai';
+import {
+  CoreMessage,
+  CoreUserMessage,
+  ImagePart,
+  TextPart,
+  ToolExecutionOptions,
+  jsonSchema,
+  streamText,
+  tool,
+} from 'ai';
 
 import { Autowired, Injectable } from '@opensumi/di';
 import { IAIBackServiceOption } from '@opensumi/ide-core-common';
@@ -48,6 +57,7 @@ export abstract class BaseLanguageModel {
       options.trimTexts,
       options.system,
       options.maxTokens,
+      options.images,
       cancellationToken,
     );
   }
@@ -77,6 +87,7 @@ export abstract class BaseLanguageModel {
     trimTexts?: [string, string],
     systemPrompt?: string,
     maxTokens?: number,
+    images?: string[],
     cancellationToken?: CancellationToken,
   ): Promise<any> {
     try {
@@ -89,7 +100,18 @@ export abstract class BaseLanguageModel {
         });
       }
 
-      const messages: CoreMessage[] = [...history, { role: 'user', content: request }];
+      const messages: CoreMessage[] = [
+        ...history,
+        {
+          role: 'user',
+          content: images?.length
+            ? [
+                { type: 'text', text: request } as TextPart,
+                ...images.map((image) => ({ type: 'image', image: new URL(image) } as ImagePart)),
+              ]
+            : request,
+        },
+      ];
       const modelInfo = modelId ? this.getModelInfo(modelId) : undefined;
       const stream = streamText({
         model: this.getModelIdentifier(provider, modelId),
@@ -101,9 +123,9 @@ export abstract class BaseLanguageModel {
         maxTokens,
         temperature: modelInfo?.temperature || 0,
         topP: modelInfo?.topP || 0.8,
-        topK: modelInfo?.topK || 1,
         system: systemPrompt,
         providerOptions,
+        ...(!images?.length && { topK: modelInfo?.topK || 1 }),
       });
 
       // 状态跟踪变量
diff --git a/packages/components/package.json b/packages/components/package.json
index 8c2336e410..e1b66dd8f8 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -26,6 +26,7 @@
     "raf": "^3.4.1",
     "rc-dialog": "^9.6.0",
     "rc-dropdown": "~2.4.1",
+    "rc-image": "^7.11.0",
     "rc-input-number": "^9.3.0",
     "rc-menu": "^9.16.0",
     "rc-notification": "~3.3.1",
diff --git a/packages/components/src/image/index.tsx b/packages/components/src/image/index.tsx
new file mode 100644
index 0000000000..467951d46d
--- /dev/null
+++ b/packages/components/src/image/index.tsx
@@ -0,0 +1,24 @@
+import RcImage, { ImageProps as RcImageProps } from 'rc-image';
+import React from 'react';
+
+import './styles.less';
+import { Icon } from '../icon';
+
+export interface ImageProps extends RcImageProps {
+  className?: string;
+}
+
+export const Image = (props: ImageProps) => (
+  <RcImage
+    prefixCls='kt-image'
+    preview={{
+      getContainer: () => document.getElementById('main') || document.getElementsByTagName('body')?.[0],
+      mask: (
+        <div className='mask'>
+          <Icon iconClass='codicon codicon-eye' />
+        </div>
+      ),
+    }}
+    {...props}
+  />
+);
diff --git a/packages/components/src/image/styles.less b/packages/components/src/image/styles.less
new file mode 100644
index 0000000000..47cd89db65
--- /dev/null
+++ b/packages/components/src/image/styles.less
@@ -0,0 +1,205 @@
+@import '../style/variable.less';
+@import '../style/mixins.less';
+
+@image-prefix-cls: ~'kt-image';
+@image-preview-prefix-cls: ~'@{image-prefix-cls}-preview';
+
+.@{image-prefix-cls} {
+  position: relative;
+  display: inline-block;
+
+  &-img {
+    width: 100%;
+    height: auto;
+    vertical-align: middle;
+
+    &-placeholder {
+      background-color: @image-bg;
+      background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTQuNSAyLjVoLTEzQS41LjUgMCAwIDAgMSAzdjEwYS41LjUgMCAwIDAgLjUuNWgxM2EuNS41IDAgMCAwIC41LS41VjNhLjUuNSAwIDAgMC0uNS0uNXpNNS4yODEgNC43NWExIDEgMCAwIDEgMCAyIDEgMSAwIDAgMSAwLTJ6bTguMDMgNi44M2EuMTI3LjEyNyAwIDAgMS0uMDgxLjAzSDIuNzY5YS4xMjUuMTI1IDAgMCAxLS4wOTYtLjIwN2wyLjY2MS0zLjE1NmEuMTI2LjEyNiAwIDAgMSAuMTc3LS4wMTZsLjAxNi4wMTZMNy4wOCAxMC4wOWwyLjQ3LTIuOTNhLjEyNi4xMjYgMCAwIDEgLjE3Ny0uMDE2bC4wMTUuMDE2IDMuNTg4IDQuMjQ0YS4xMjcuMTI3IDAgMCAxLS4wMi4xNzV6IiBmaWxsPSIjOEM4QzhDIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48L3N2Zz4=');
+      background-repeat: no-repeat;
+      background-position: center center;
+      background-size: 30%;
+    }
+  }
+
+  &-mask {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: @text-color-inverse;
+    background: fade(@black, 50%);
+    cursor: pointer;
+    opacity: 0;
+    transition: opacity @animation-duration-slow;
+
+    &-info {
+      padding: 0 @padding-xss;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      .@{iconfont-css-prefix} {
+        margin-inline-end: @margin-xss;
+      }
+    }
+
+    &:hover {
+      opacity: 1;
+    }
+  }
+
+  &-placeholder {
+    .box();
+  }
+
+  &-preview {
+    .modal-mask();
+
+    height: 100%;
+    text-align: center;
+
+    &-body {
+      .box();
+      overflow: hidden;
+    }
+
+    &-img {
+      max-width: 100%;
+      max-height: 100%;
+      vertical-align: middle;
+      transform: scale3d(1, 1, 1);
+      cursor: grab;
+      transition: transform 0.3s @ease-out 0s;
+      user-select: none;
+      pointer-events: auto;
+
+      &-wrapper {
+        .box();
+        transition: transform 0.3s @ease-out 0s;
+
+        &::before {
+          display: inline-block;
+          width: 1px;
+          height: 50%;
+          margin-right: -1px;
+          content: '';
+        }
+      }
+    }
+
+    &-moving {
+      .@{image-prefix-cls}-preview-img {
+        cursor: grabbing;
+
+        &-wrapper {
+          transition-duration: 0s;
+        }
+      }
+    }
+
+    &-wrap {
+      z-index: @zindex-image;
+    }
+
+    // 暂时不需要控制
+    &-operations-wrapper {
+      display: none;
+      //   position: fixed;
+      //   top: 0;
+      //   right: 0;
+      //   z-index: @zindex-image + 1;
+      //   width: 100%;
+    }
+
+    &-operations {
+      .reset-component();
+      display: flex;
+      flex-direction: row-reverse;
+      align-items: center;
+      color: @image-preview-operation-color;
+      list-style: none;
+      background: fade(@modal-mask-bg, 10%);
+      pointer-events: auto;
+
+      &-operation {
+        margin-left: @control-padding-horizontal;
+        padding: @control-padding-horizontal;
+        cursor: pointer;
+        transition: all 0.3s;
+
+        &:hover {
+          background: fade(@modal-mask-bg, 20%);
+        }
+
+        &-disabled {
+          color: @image-preview-operation-disabled-color;
+          pointer-events: none;
+        }
+
+        &:last-of-type {
+          margin-left: 0;
+        }
+      }
+
+      &-progress {
+        position: absolute;
+        left: 50%;
+        transform: translateX(-50%);
+      }
+
+      &-icon {
+        font-size: @image-preview-operation-size;
+      }
+    }
+
+    &-switch-left,
+    &-switch-right {
+      position: fixed;
+      top: 50%;
+      right: 8px;
+      z-index: @zindex-image + 1;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 44px;
+      height: 44px;
+      color: @image-preview-operation-color;
+      background: fade(@modal-mask-bg, 10%);
+      border-radius: 50%;
+      transform: translateY(-50%);
+      cursor: pointer;
+      transition: all 0.3s;
+      pointer-events: auto;
+
+      &:hover {
+        background: fade(@modal-mask-bg, 20%);
+      }
+
+      &-disabled,
+      &-disabled:hover {
+        color: @image-preview-operation-disabled-color;
+        background: fade(@modal-mask-bg, 10%);
+        cursor: not-allowed;
+        > .@{iconfont-css-prefix} {
+          cursor: not-allowed;
+        }
+      }
+
+      > .@{iconfont-css-prefix} {
+        font-size: 18px;
+      }
+    }
+
+    &-switch-left {
+      left: 8px;
+    }
+
+    &-switch-right {
+      right: 8px;
+    }
+  }
+}
diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts
index 343e4adde5..28df770f92 100644
--- a/packages/components/src/index.ts
+++ b/packages/components/src/index.ts
@@ -9,6 +9,7 @@ export * from './tabs';
 export * from './overlay';
 export * from './dialog';
 export * from './icon';
+export * from './image';
 export * from './notification';
 export * from './popover';
 export * from './message';
diff --git a/packages/components/src/style/mixins.less b/packages/components/src/style/mixins.less
index 6f2c91b635..e082f796e2 100644
--- a/packages/components/src/style/mixins.less
+++ b/packages/components/src/style/mixins.less
@@ -133,3 +133,40 @@
     clear: both;
   }
 }
+
+.box(@position: absolute) {
+  position: @position;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+}
+
+.modal-mask() {
+  pointer-events: none;
+
+  &.@{prefix}-zoom-enter,
+  &.@{prefix}-zoom-appear {
+    transform: none; // reset scale avoid mousePosition bug
+    opacity: 0;
+    animation-duration: @animation-duration-slow;
+    user-select: none; // https://github.com/ant-design/ant-design/issues/11777
+  }
+
+  &-mask {
+    .box(fixed);
+    z-index: @zindex-modal-mask;
+    height: 100%;
+    background-color: @modal-mask-bg;
+
+    &-hidden {
+      display: none;
+    }
+  }
+
+  &-wrap {
+    .box(fixed);
+    overflow: auto;
+    outline: 0;
+  }
+}
diff --git a/packages/components/src/style/variable.less b/packages/components/src/style/variable.less
index 46ff8f92fe..c3e8385da5 100644
--- a/packages/components/src/style/variable.less
+++ b/packages/components/src/style/variable.less
@@ -59,6 +59,7 @@
 @zindex-dropdown: 1050;
 @zindex-picker: 1050;
 @zindex-tooltip: 1060;
+@zindex-image: 1080;
 
 // Shadow
 @shadow-color: rgba(0, 0, 0, 0.15);
@@ -112,6 +113,14 @@
 @padding-md: 16px; // small containers and buttons
 @padding-sm: 12px; // Form controls and items
 @padding-xs: 8px; // small items
+@padding-xss: 4px; // more small
+
+// vertical margins
+@margin-lg: 24px; // containers
+@margin-md: 16px; // small containers and buttons
+@margin-sm: 12px; // Form controls and items
+@margin-xs: 8px; // small items
+@margin-xss: 4px; // more small
 
 // vertical padding for all form controls
 @control-padding-horizontal: @padding-sm;
@@ -205,3 +214,13 @@
 // Checkbox
 @checkbox-default-size: 12px;
 @checkbox-large-size: 14px;
+
+// Image
+@image-size-base: 48px;
+@image-font-size-base: 24px;
+@image-bg: #f5f5f5;
+@image-color: #fff;
+@image-mask-font-size: 16px;
+@image-preview-operation-size: 18px;
+@image-preview-operation-color: @text-color-dark;
+@image-preview-operation-disabled-color: fade(@image-preview-operation-color, 25%);
diff --git a/packages/core-browser/src/components/ai-native/enhanceIcon/styles.module.less b/packages/core-browser/src/components/ai-native/enhanceIcon/styles.module.less
index 726230dd56..2d8c01e90a 100644
--- a/packages/core-browser/src/components/ai-native/enhanceIcon/styles.module.less
+++ b/packages/core-browser/src/components/ai-native/enhanceIcon/styles.module.less
@@ -4,7 +4,6 @@
   display: flex;
   align-items: center;
   cursor: pointer;
-  text-wrap: nowrap;
   white-space: nowrap;
   box-sizing: border-box;
 
diff --git a/packages/core-common/src/types/ai-native/index.ts b/packages/core-common/src/types/ai-native/index.ts
index 4bb1bc1aa4..66ac85a138 100644
--- a/packages/core-common/src/types/ai-native/index.ts
+++ b/packages/core-common/src/types/ai-native/index.ts
@@ -171,6 +171,7 @@ export interface IAIBackServiceOption {
   type?: string;
   requestId?: string;
   sessionId?: string;
+  images?: string[];
   history?: CoreMessage[];
   tools?: any[];
   clientId?: string;
@@ -440,6 +441,7 @@ export interface IHistoryChatMessage extends IChatMessage {
   order: number;
 
   type?: 'string' | 'component';
+  images?: string[];
   relationId?: string;
   componentId?: string;
   componentValue?: any;
diff --git a/packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts b/packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts
index 8e63f9ab71..ad3b80a750 100644
--- a/packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts
+++ b/packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts
@@ -20,11 +20,9 @@ import {
   IRenameCandidatesProviderRegistry,
   IResolveConflictRegistry,
   ITerminalProviderRegistry,
-  TChatSlashCommandSend,
   TerminalSuggestionReadableStream,
 } from '@opensumi/ide-ai-native/lib/browser/types';
 import { InlineChatController } from '@opensumi/ide-ai-native/lib/browser/widget/inline-chat/inline-chat-controller';
-import { SerializedContext } from '@opensumi/ide-ai-native/lib/common/llm-context';
 import {
   ChatAgentPromptProvider,
   DefaultChatAgentPromptProvider,
@@ -48,8 +46,6 @@ import {
 import { ICodeEditor, ISelection, NewSymbolName, NewSymbolNameTag, Range, Selection } from '@opensumi/ide-monaco';
 import { MarkdownString } from '@opensumi/monaco-editor-core/esm/vs/base/common/htmlContent';
 
-import { SlashCommand } from './SlashCommand';
-
 export enum EInlineOperation {
   Comments = 'Comments',
   Optimize = 'Optimize',
@@ -236,6 +232,10 @@ export class AINativeContribution implements AINativeCoreContribution {
       ],
     );
 
+    registry.registerImageUploadProvider({
+      imageUpload: imageToBase64,
+    });
+
     // registry.registerSlashCommand(
     //   {
     //     name: 'Explain',
@@ -509,3 +509,23 @@ export class AINativeContribution implements AINativeCoreContribution {
     });
   }
 }
+
+const MAX_IMAGE_SIZE = 3 * 1024 * 1024;
+
+const imageToBase64 = (file: File) =>
+  new Promise<string>((resolve, reject) => {
+    if (file.size > MAX_IMAGE_SIZE) {
+      reject(new Error('Image size exceeds 3MB limit'));
+      return;
+    }
+
+    const reader = new FileReader();
+    reader.onload = () => {
+      const base64String = reader.result as string;
+      resolve(base64String);
+    };
+    reader.onerror = () => {
+      reject(new Error('Failed to convert image to base64'));
+    };
+    reader.readAsDataURL(file);
+  });
diff --git a/yarn.lock b/yarn.lock
index f06e062f4a..030082b716 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3490,6 +3490,7 @@ __metadata:
     raf: "npm:^3.4.1"
     rc-dialog: "npm:^9.6.0"
     rc-dropdown: "npm:~2.4.1"
+    rc-image: "npm:^7.11.0"
     rc-input-number: "npm:^9.3.0"
     rc-menu: "npm:^9.16.0"
     rc-notification: "npm:~3.3.1"
@@ -20350,7 +20351,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"rc-image@npm:~7.11.0":
+"rc-image@npm:^7.11.0, rc-image@npm:~7.11.0":
   version: 7.11.0
   resolution: "rc-image@npm:7.11.0"
   dependencies:


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 8**

<details>
<summary>🧹 Nitpick comments (33)</summary><blockquote>

<details>
<summary>packages/ai-native/src/browser/context/llm-context.service.ts (8)</summary><blockquote>

`32-33`: **新增 IFileServiceClient 依赖项。**
使用前建议评估现有错误处理逻辑，确保异步 fileService 调用过程中不会遗漏异常捕获。

---

`40-40`: **将 maxAttachFoldersLimit 提取为可配置常量。**
如果未来需要灵活调整或根据不同场景设定更大/更小的上限，放置在可配置信息（例如配置文件或环境变量）中更为恰当。

---

`42-44`: **可进一步统一命名，避免后期维护歧义。**
目前 attachedFiles、attachedFolders、attachedImages 三者概念相似但略有差异，建议保持命名一致或在注释中明确说明用途。另观察到 attachedImages 尚未实际使用，可确认后续实现计划。

---

`65-75`: **与 addFileToList 存在明显重复逻辑，建议抽取公共方法。**
当前代码几乎重复了查重、删除、追加逻辑，可将两者通用部分汇总为一个私有抽象函数，提升可维护性。

---

`94-103`: **变量命名与方法目标不符。**
函数内部将 folder 存储在名为 file 的常量中，易引发混淆。建议改用更贴切的命名，例如 const folder = { uri }。

---

`197-206`: **异步序列化方法合理，但需注意大规模文件夹影响整体性能。**
深层或大型文件夹在遍历时可能占用较长时间，建议在调用或内部对文件夹大小、层级进行限制或分批处理。

---

`208-220`: **对文件夹路径去重并逐一转换为结构概览，性能消耗需谨慎。**
若存在大量文件夹或深层次目录，循环多次 fileStat 调用可能冗长，可考虑预先做数量过滤或更多限制。

---

`222-252`: **函数名应为 getPartialFolderStructure 并建议补充错误捕获。**
“getPartiaFolderStructure” 存在拼写问题；同时对 fileService 异步调用建议使用 try/catch，防止意外抛出阻断后续流程。

```diff
- private async getPartiaFolderStructure(folder: string, level = 2): Promise<string[]> {
+ private async getPartialFolderStructure(folder: string, level = 2): Promise<string[]> {
```

</blockquote></details>
<details>
<summary>packages/ai-native/src/browser/components/mention-input/mention-item.tsx (1)</summary><blockquote>

`1-24`: **MentionItem 组件实现良好**

这个新组件实现了提及项的渲染，包括图标、文本和描述等信息，使用 `classnames` 实现条件样式应用，代码结构清晰，并且适当地处理了嵌套项的情况。作为提及输入功能的一部分，该组件有助于实现文件和图片的引用功能。


不过，建议考虑以下几点优化：

1. 可以使用 React.memo 包装组件以避免不必要的重渲染
2. 为点击事件处理添加键盘可访问性支持（如 onKeyDown 处理 Enter 键）

```diff
-export const MentionItem: React.FC<MentionItemProps> = ({ item, isActive, onClick }) => (
+export const MentionItem: React.FC<MentionItemProps> = React.memo(({ item, isActive, onClick }) => (
-  <div className={`${styles.mention_item} ${isActive ? styles.active : ''}`} onClick={() => onClick(item)}>
+  <div 
+    className={`${styles.mention_item} ${isActive ? styles.active : ''}`} 
+    onClick={() => onClick(item)}
+    onKeyDown={(e) => e.key === 'Enter' && onClick(item)}
+    tabIndex={0}
+    role="option"
+    aria-selected={isActive}
+  >
    <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>
-);
+));
```

</blockquote></details>
<details>
<summary>packages/ai-native/src/browser/contrib/image-upload/image-upload.feature.registry.ts (1)</summary><blockquote>

`1-19`: **实现图片上传提供者注册机制**

创建了 `ImageUploadProviderRegistry` 类来管理图片上传提供者。此类提供了注册和获取图片上传提供者的方法，使系统能够灵活地集成不同的图片上传实现。代码结构清晰，遵循了依赖注入模式。

目前实现只支持单个上传提供者，未来可能需要考虑支持多个提供者的场景。



如果将来需要支持多个提供者，可以考虑以下实现：

```diff
- private imageUploadProvider: IImageUploadProvider | undefined;
+ private imageUploadProviders: Map<string, IImageUploadProvider> = new Map();

- registerImageUploadProvider(provider: IImageUploadProvider): void {
-   this.imageUploadProvider = provider;
+ registerImageUploadProvider(id: string, provider: IImageUploadProvider): void {
+   this.imageUploadProviders.set(id, provider);
  }

- getImageUploadProvider(): IImageUploadProvider | undefined {
-   return this.imageUploadProvider;
+ getImageUploadProvider(id?: string): IImageUploadProvider | undefined {
+   if (id) {
+     return this.imageUploadProviders.get(id);
+   }
+   // 返回默认提供者或第一个注册的提供者
+   return this.imageUploadProviders.values().next().value;
  }
```

</blockquote></details>
<details>
<summary>packages/ai-native/src/browser/components/mention-input/mention-panel.tsx (1)</summary><blockquote>

`1-89`: **实现提及菜单面板组件**

`MentionPanel` 组件用于显示提及建议列表，支持筛选、滚动和选择功能。组件实现了以下功能：
1. 根据过滤文本动态筛选列表项
2. 支持多级菜单（通过level属性）
3. 自动滚动到活动项
4. 显示加载状态和无结果提示

代码结构良好，React Hooks 使用恰当。

建议对过滤逻辑和无结果显示逻辑进行优化：



```diff
- 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();
+ const filteredItems = React.useMemo(() => {
+   let result = items;
+   
+   if (level === 0 && filter && filter.length > 1) {
+     // 一级菜单根据 @ 后面的文本过滤
+     const searchText = filter.substring(1).toLowerCase();
+     result = items.filter((item) => item.text.toLowerCase().includes(searchText));
+   } else if (level !== 0 && filter && filter.length > 0) {
+     // 二级菜单根据 @file: 后面的文本过滤
+     result = items.filter((item) => item.text.toLowerCase().includes(filter.toLowerCase()));
+   }
+   
+   return result;
+ }, [items, filter, level]);
```

同时，改进列表渲染逻辑：

```diff
- {items.length > 0 ? (
+ {filteredItems.length > 0 ? (
    <ul className={styles.mention_list}>
-     {items.map((item, index) => (
+     {filteredItems.map((item, index) => (
        <MentionItem
          key={item.id}
          item={item}
          isActive={index === activeIndex}
          onClick={() => onSelectItem(item, true)}
        />
      ))}
    </ul>
  ) : (
    <div className={styles.no_results}>{loading ? '正在搜索...' : '无匹配结果'}</div>
  )}
```

</blockquote></details>
<details>
<summary>packages/ai-native/src/browser/components/components.module.less (3)</summary><blockquote>

`7-9`: **请确认新的定位偏移不会导致界面元素重叠或错位**  
`bottom: -15px` 和 `left: -9px` 相比原先数值有所变动，建议在小分辨率或特殊布局下多做验证，确保不会造成不期望的遮挡或重叠。

---

`519-519`: **为 `.model_selector` 设置最小宽度时，需留意宽度自适应策略**  
`min-width: 150px` 能保证下拉选择器在小屏幕上保持一定可视范围，但建议在更狭窄的屏幕上验证样式，避免出现溢出等问题。

---

`569-639`: **新增 `.attachment`、`.attachment_text` 与 `.thumbnail_container` 样式有助于增强可读性，但需审视删除按钮的定位**  
删除按钮的 `position: absolute; top: -5px; right: -5px;` 可能在缩放或容器较小的场景下点击区域不灵敏，或出现图标局部不可见的情况，建议在容器环境中验证。

</blockquote></details>
<details>
<summary>packages/ai-native/src/browser/components/mention-input/mention-input.module.less (1)</summary><blockquote>

`46-73`: **交互式图标的悬浮效果可能需要进一步对比度优化**  
`.ai_enhance_icon` 中设置的 `background-color: var(--badge-background)` 及 `color: var(--badge-foreground)` 在深色主题或浅色主题下，需要保证仍能明显区分图标悬浮态。

</blockquote></details>
<details>
<summary>packages/ai-native/src/browser/components/mention-input/mention-input.tsx (3)</summary><blockquote>

`14-44`: **多层级的 mentionState 状态可读性高，但内部字段较多**  
如果项目不断拓展，建议考虑拆分或重构这部分逻辑，使用自定义 Hook 或 context 便于协同管理和单元测试。

---

`176-277`: **handleInput 对输入框内容做了多重判断，能防止无效渲染**  
其中对空标签 `<br>` 的处理非常实用，但要谨防部分浏览器的兼容差异，如过去版本的 Firefox 等。建议做一次多浏览器测试以确认行为一致。

---

`294-458`: **键盘事件处理兼顾 ESC、上下箭头、多级菜单和历史记录**  
逻辑丰富，但若后续再扩展更多组合键处理，会进一步增加维护难度。可考虑把导航逻辑提取到单独的函数或自定义 Hook，以提升可读性和可测试性。

</blockquote></details>
<details>
<summary>packages/ai-native/src/browser/chat/chat-agent.service.ts (2)</summary><blockquote>

`77-77`: **监听上下文文件变更事件。**  
当上下文文件版本变更时，标记 `shouldUpdateContext = true`。若此流程后续出错，建议考虑在事件处理环节添加异常捕获和相应的容错处理逻辑。

---

`165-167`: **异步方法 `provideContextMessage` 实现上下文注入逻辑。**  
使用 `llmContextService.serialize()` 搭配 `promptProvider.provideContextPrompt` 生成完整消息，功能明确。若远程或序列化过程可能失败，建议在此添加错误捕获，防止阻塞后续流程。

</blockquote></details>
<details>
<summary>packages/ai-native/src/browser/components/ChatMentionInput.tsx (1)</summary><blockquote>

`24-49`: **新定义的 `IChatMentionInputProps` 接口。**  
- 属性命名语义明确，如 `onSend`, `onValueChange`, `onExpand` 等。  
- 支持外部注入 `images`、`agentId`、`command` 等，扩展性好。  
- 建议在注释中说明各属性的可选与默认值，方便后续维护。

</blockquote></details>
<details>
<summary>packages/ai-native/src/browser/chat/chat.view.tsx (6)</summary><blockquote>

`24-24`: **引入 `CommandService` 合理，但请确认实际使用场景。**  
`CommandService` 的注入一般在需要触发命令执行的场景下使用，若仅是占位请确认后续是否有用到。

---

`118-119`: **在组件中注入 `aiNativeConfigService` 与 `llmContextService`**  
确保在组件卸载时，可以正确释放事件或监听，避免内存泄漏。

---

`129-132`: **注入 `labelService`、`workspaceService`、`commandService`**  
这些服务调用频率较高，建议统一在一个逻辑层中管理，以保证代码可读性。

---

`202-205`: **根据配置条件选择输入组件的逻辑可读性良好。**  
若未来配置项增加，适当抽离该条件判断为独立函数可能更清晰。

---

`278-283`: **异步调用 `handleSend` 使用 `await` 符合需求，但要注意并发场景。**  
若存在多次快速发送的需求，可在函数内部增加节流/队列逻辑，避免潜在并发问题。

---

`463-474`: **`renderUserMessage` 中对用户消息渲染加入 `images` 参数是有用的。**  
建议后续补充对空数组或超大图列表的处理，防止渲染异常。

</blockquote></details>
<details>
<summary>packages/ai-native/src/common/llm-context.ts (2)</summary><blockquote>

`6-15`: **`startAutoCollection` 和 `stopAutoCollection` 的接口命名清晰，可考虑记录当前状态。**  
建议增加一个内部状态标记，方便排查在自动模式中多次调用开始或停止的情况。

---

`63-63`: **`attachedFolders: string[]` 属性是关键点。**  
后续若需要路径按层次组织，可以考虑改为更丰富的结构体。

</blockquote></details>
<details>
<summary>packages/ai-native/src/browser/types.ts (3)</summary><blockquote>

`144-147`: **`ChatUserRoleRender` 接受可选 `images` 属性，拓展了渲染场景。**  
需注意在渲染多张图片时的布局和性能，避免阻塞 UI。

---

`294-297`: **`IImageUploadProvider` 接口的定义明确，可扩展性不错。**  
建议对 `imageUpload` 的上传错误进行更详细的处理或回退逻辑。

---

`298-302`: **`IImageUploadProviderRegistry` 支持注册/获取单个上传提供者。**  
若后续需要多种上传策略，可考虑支持多提供者并按优先级选择。

</blockquote></details>
<details>
<summary>packages/ai-native/src/browser/components/mention-input/types.ts (1)</summary><blockquote>

`26-38`: **提及状态管理设计**

`MentionState` 接口定义了提及组件的状态管理结构，包括活动状态、位置、过滤文本等。注释用中文编写清晰地说明了 `level` 字段的含义，但建议其他字段也添加类似的注释以提高可读性。

建议为其他重要字段添加简短注释，特别是对于不太直观的属性，例如 `inlineSearchActive` 和 `secondLevelFilter` 等。

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1725b0d09ad42e0fafc649fa301c4c3b56658a41 and 3fd08bebe9118cc00b348a26f329b6af5e6c13fb.

</details>

<details>
<summary>⛔ Files ignored due to path filters (5)</summary>

* `packages/components/src/icon/iconfont/iconfont.eot` is excluded by `!**/*.eot`
* `packages/components/src/icon/iconfont/iconfont.svg` is excluded by `!**/*.svg`
* `packages/components/src/icon/iconfont/iconfont.ttf` is excluded by `!**/*.ttf`
* `packages/components/src/icon/iconfont/iconfont.woff` is excluded by `!**/*.woff`
* `packages/components/src/icon/iconfont/iconfont.woff2` is excluded by `!**/*.woff2`

</details>

<details>
<summary>📒 Files selected for processing (44)</summary>

* `package.json` (1 hunks)
* `packages/ai-native/__test__/browser/chat/chat-agent.service.test.ts` (1 hunks)
* `packages/ai-native/src/browser/ai-core.contribution.ts` (4 hunks)
* `packages/ai-native/src/browser/chat/chat-agent.service.ts` (3 hunks)
* `packages/ai-native/src/browser/chat/chat-manager.service.ts` (3 hunks)
* `packages/ai-native/src/browser/chat/chat-proxy.service.ts` (1 hunks)
* `packages/ai-native/src/browser/chat/chat.internal.service.ts` (1 hunks)
* `packages/ai-native/src/browser/chat/chat.view.tsx` (16 hunks)
* `packages/ai-native/src/browser/components/ChatEditor.tsx` (7 hunks)
* `packages/ai-native/src/browser/components/ChatMentionInput.tsx` (1 hunks)
* `packages/ai-native/src/browser/components/ChatThinking.tsx` (3 hunks)
* `packages/ai-native/src/browser/components/change-list.module.less` (1 hunks)
* `packages/ai-native/src/browser/components/chat-context/index.tsx` (1 hunks)
* `packages/ai-native/src/browser/components/components.module.less` (3 hunks)
* `packages/ai-native/src/browser/components/mention-input/mention-input.module.less` (1 hunks)
* `packages/ai-native/src/browser/components/mention-input/mention-input.tsx` (1 hunks)
* `packages/ai-native/src/browser/components/mention-input/mention-item.tsx` (1 hunks)
* `packages/ai-native/src/browser/components/mention-input/mention-panel.tsx` (1 hunks)
* `packages/ai-native/src/browser/components/mention-input/types.ts` (1 hunks)
* `packages/ai-native/src/browser/context/llm-context.service.ts` (6 hunks)
* `packages/ai-native/src/browser/contrib/image-upload/image-upload.feature.registry.ts` (1 hunks)
* `packages/ai-native/src/browser/index.ts` (2 hunks)
* `packages/ai-native/src/browser/model/msg-history-manager.ts` (1 hunks)
* `packages/ai-native/src/browser/types.ts` (4 hunks)
* `packages/ai-native/src/common/index.ts` (3 hunks)
* `packages/ai-native/src/common/llm-context.ts` (3 hunks)
* `packages/ai-native/src/common/prompts/context-prompt-provider.ts` (2 hunks)
* `packages/ai-native/src/node/base-language-model.ts` (4 hunks)
* `packages/components/src/icon/iconfont/iconMap.ts` (4 hunks)
* `packages/components/src/icon/iconfont/iconfont.css` (3 hunks)
* `packages/components/src/icon/iconfont/iconfont.html` (3 hunks)
* `packages/components/src/markdown-react/parse.tsx` (4 hunks)
* `packages/core-browser/src/ai-native/ai-config.service.ts` (0 hunks)
* `packages/core-browser/src/style/icon/ide-iconfont.ts` (1 hunks)
* `packages/core-common/src/types/ai-native/index.ts` (2 hunks)
* `packages/design/src/browser/style/global.less` (1 hunks)
* `packages/file-search/src/common/file-search.ts` (1 hunks)
* `packages/file-search/src/node/file-search.service.ts` (5 hunks)
* `packages/file-tree-next/src/browser/file-tree-contribution.ts` (1 hunks)
* `packages/i18n/src/common/en-US.lang.ts` (2 hunks)
* `packages/i18n/src/common/zh-CN.lang.ts` (2 hunks)
* `packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts` (2 hunks)
* `packages/theme/src/common/color-tokens/chatColors.ts` (1 hunks)
* `packages/theme/src/common/color-tokens/index.ts` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* packages/core-browser/src/ai-native/ai-config.service.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>packages/core-common/src/types/ai-native/index.ts</summary>

[error] 444-447: The enum declaration should not be const

Const enums are not supported by bundlers and are incompatible with the 'isolatedModules' mode. Their use can lead to import inexistent values.
See TypeScript Docs for more details.
Safe fix: Turn the const enum into a regular enum.


(lint/suspicious/noConstEnum)

</details>
<details>
<summary>packages/ai-native/src/browser/components/ChatEditor.tsx</summary>

[error] 169-169: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it \[.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)

---

[error] 170-170: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it \[.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)

---

[error] 189-189: Unnecessary continue statement

Unsafe fix: Delete the unnecessary continue statement


(lint/correctness/noUnnecessaryContinue)

</details>

</details>
<details>
<summary>🪛 GitHub Check: unittest (ubuntu-latest, 18.x, jsdom)</summary>

<details>
<summary>packages/ai-native/src/browser/chat/chat.view.tsx</summary>

[failure] 866-866: 
Type '(message: string, images?: string[], agentId?: string, command?: string) => Promise<void>' is not assignable to type '((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined, op...'.

</details>

</details>
<details>
<summary>🪛 GitHub Check: unittest (ubuntu-latest, 18.x, node)</summary>

<details>
<summary>packages/ai-native/src/browser/chat/chat.view.tsx</summary>

[failure] 866-866: 
Type '(message: string, images?: string[], agentId?: string, command?: string) => Promise<void>' is not assignable to type '((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined, op...'.

</details>

</details>
<details>
<summary>🪛 GitHub Check: build (ubuntu-latest, 20.x)</summary>

<details>
<summary>packages/ai-native/src/browser/chat/chat.view.tsx</summary>

[failure] 866-866: 
Type '(message: string, images?: string[], agentId?: string, command?: string) => Promise<void>' is not assignable to type '((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined, op...'.

</details>

</details>
<details>
<summary>🪛 GitHub Check: unittest (macos-latest, 18.x, jsdom)</summary>

<details>
<summary>packages/ai-native/src/browser/chat/chat.view.tsx</summary>

[failure] 866-866: 
Type '(message: string, images?: string[], agentId?: string, command?: string) => Promise<void>' is not assignable to type '((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined, op...'.

</details>

</details>
<details>
<summary>🪛 GitHub Check: ubuntu-latest, Node.js 20.x</summary>

<details>
<summary>packages/ai-native/src/browser/chat/chat.view.tsx</summary>

[failure] 866-866: 
Type '(message: string, images?: string[], agentId?: string, command?: string) => Promise<void>' is not assignable to type '((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined, op...'.

</details>

</details>
<details>
<summary>🪛 GitHub Actions: E2E Test</summary>

<details>
<summary>packages/ai-native/src/browser/chat/chat.view.tsx</summary>

[error] 866-866: Type '(message: string, images?: string[], agentId?: string, command?: string) => Promise<void>' is not assignable to type '((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined, op...'.

</details>

</details>
<details>
<summary>🪛 GitHub Actions: CI</summary>

<details>
<summary>packages/ai-native/src/browser/chat/chat.view.tsx</summary>

[error] 866-866: error TS2322: Type '(message: string, images?: string[], agentId?: string, command?: string) => Promise<void>' is not assignable to type '((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined, op...'.

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: WIP
* GitHub Check: WIP
* GitHub Check: build-windows

</details>

<details>
<summary>🔇 Additional comments (109)</summary><blockquote>

<details>
<summary>packages/ai-native/src/browser/components/change-list.module.less (1)</summary>

`106-107`: **增强页面布局的CSS属性调整**  
在 `.fileStats` 类中新增 `flex: 1;` 和 `justify-content: flex-end;` 属性，可以使该元素在其容器内占据剩余空间并将内容右对齐。这将有助于改善整体布局和视觉对齐效果。请确认这些变更与整体UI设计保持一致。

</details>
<details>
<summary>packages/components/src/icon/iconfont/iconMap.ts (4)</summary>

`45-45`: **添加了新图标 'codebraces'**

这是支持聊天添加图片功能的一部分，新增了代码括号图标，能够更好地表示代码块或代码片段。

---

`83-83`: **添加了新图标 'file'**

新增文件图标，可用于在聊天界面中表示文件附件，这与PR的目标"支持添加图片到聊天"相符。

---

`119-119`: **添加了新图标 'mcp'**

新增了mcp图标，可能用于多内容面板(Multi-Content Panel)或特定的控制功能，它是此次功能实现的一部分。

---

`168-168`: **将'send-hollow'重命名为'send-outlined'**

图标命名从'send-hollow'更改为'send-outlined'，保持了命名的一致性，符合outlined样式的命名惯例。

</details>
<details>
<summary>packages/components/src/icon/iconfont/iconfont.html (4)</summary>

`7-7`: **更新了图标字体引用链接**

CSS文件引用路径已更新为新版本(`font_1432262_1ef4fl5cm51.css`)，确保新添加的图标能够正确加载。

---

`88-89`: **更新了版本号和引用路径**

OpenSumi版本已更新至v3.8.1，并且相应地更新了图标字体引用路径。这保证了版本信息的一致性。

---

`95-112`: **添加了三个新图标的HTML展示元素**

为`codebraces`、`file`和`mcp`三个新图标添加了对应的HTML展示元素，使它们能在图标预览页面中正确显示，便于开发者查看和使用。

---

`143-147`: **将'send-hollow'图标重命名为'send-outlined'**

在HTML预览中也对应修改了图标名称，保持了与iconMap.ts中的命名一致性。

</details>
<details>
<summary>packages/components/src/icon/iconfont/iconfont.css (3)</summary>

`3-8`: **更新了字体文件的引用链接**

所有字体文件的引用时间戳从`t=1709176848484`更新为`t=1741593634138`，确保加载最新的图标资源。

---

`19-29`: **添加了三个新图标的CSS定义**

为`codebraces`、`file`和`mcp`三个新图标添加了相应的CSS类和Unicode内容值，使它们能够在应用中正确显示。

---

`51-51`: **将'.kticon-send-hollow'重命名为'.kticon-send-outlined'**

CSS类名从`.kticon-send-hollow`更改为`.kticon-send-outlined`，保持与其他文件中命名的一致性。

</details>
<details>
<summary>packages/ai-native/src/browser/context/llm-context.service.ts (4)</summary>

`1-1`: **导入 DataContent 用于图片处理，符合本次新增需求。**

---

`15-15`: **确保 fileService 的使用具备必要的错误处理。**
在后续异步调用时，若文件系统发生异常，需要进一步确认是否应捕获并处理异常，以防止意外崩溃或数据不一致。

---

`109-112`: **清空文件上下文时同步清理文件夹上下文，保证一致性。**
此举确保上下文中所有已附加内容都被完整重置，没有遗留信息。

---

`119-120`: **返回文件夹信息以保持与已附加文件的一致性。**
将 attachedFolders 一并纳入返回值，与整体上下文数据形成完整打包。

</details>
<details>
<summary>package.json (1)</summary>

`22-22`: **新增的构建脚本有助于组件库开发**

添加了新的构建脚本 `build:components:lib`，与现有的 `build:components` 脚本形成互补。这个脚本专门用于构建组件库，而不是分发版本，有助于支持聊天图片功能所需的组件开发。

</details>
<details>
<summary>packages/components/src/markdown-react/parse.tsx (4)</summary>

`62-64`: **代码格式调整**

将 `React.cloneElement` 调用从多行格式调整为单行格式，提高了代码的简洁性。这种格式变更不影响功能，仅为样式优化。

---

`86-88`: **代码格式调整**

表格头部行的 `React.cloneElement` 调用从多行格式变更为单行格式，与其他相似调用保持一致的代码风格。

---

`102-104`: **代码格式调整**

表格主体行的 `React.cloneElement` 调用同样调整为单行格式，保持了整个文件中此类调用的一致性。

---

`188-188`: **代码格式调整**

将 `replace` 方法链调整为单行格式，与前面的格式变更保持一致，提高代码的可读性和一致性。

</details>
<details>
<summary>packages/design/src/browser/style/global.less (1)</summary>

`221-221`: **增加模态框内边距改善用户体验**

为模态框主体添加了 24px 的内边距，这将提供更好的内容间距和视觉层次，特别是当模态框中包含图片内容时，能够确保内容不会过于紧凑，提升用户体验。

</details>
<details>
<summary>packages/file-search/src/common/file-search.ts (1)</summary>

`28-28`: **新增文件夹筛选选项支持图片上传场景**

在 `BaseOptions` 接口中添加了 `onlyFolders` 可选属性，允许文件搜索服务专门针对文件夹进行搜索。这个功能对于支持图片上传到聊天中非常有用，用户可以更容易地浏览和选择图片所在的文件夹位置。

</details>
<details>
<summary>packages/theme/src/common/color-tokens/index.ts (1)</summary>

`38-38`: **导出 chatColors 模块的颜色令牌**

这个变更添加了对 `./chatColors` 模块的导出，使聊天相关的颜色令牌可以从索引文件访问，符合当前代码库的模块组织模式。

</details>
<details>
<summary>packages/ai-native/src/browser/components/chat-context/index.tsx (1)</summary>

`18-18`: **更新了文件导入路径**

将 `ContextSelector` 组件的导入路径从 `./ContextSelector` 更改为 `./context-selector`，这是从 PascalCase 命名转向 kebab-case 命名的标准化重构。

</details>
<details>
<summary>packages/ai-native/src/browser/chat/chat-proxy.service.ts (1)</summary>

`154-154`: **添加了对图片数据的支持**

将 `images: request.images` 添加到传递给 `requestStream` 方法的对象中，使聊天请求能够包含图片数据。这个更改与 PR 的目标（支持在聊天中添加图片）保持一致。

</details>
<details>
<summary>packages/core-common/src/types/ai-native/index.ts (2)</summary>

`174-174`: **添加了图片支持到 AI 服务请求选项**

为 `IAIBackServiceOption` 接口添加了可选的 `images` 属性，类型为 `string[]`，允许在 AI 后端服务请求中包含多个图片引用。

---

`444-444`: **添加了图片支持到历史聊天消息**

为 `IHistoryChatMessage` 接口添加了可选的 `images` 属性，类型为 `string[]`，使聊天历史记录能够保存和显示消息中包含的图片。

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 444-447: The enum declaration should not be const

Const enums are not supported by bundlers and are incompatible with the 'isolatedModules' mode. Their use can lead to import inexistent values.
See TypeScript Docs for more details.
Safe fix: Turn the const enum into a regular enum.


(lint/suspicious/noConstEnum)

</details>

</details>

</details>
<details>
<summary>packages/core-browser/src/style/icon/ide-iconfont.ts (1)</summary>

`1-1`: **图标字体资源已更新**

此更改更新了图标字体的 CSS 文件 URL，这很可能是为了支持新的聊天图片上传功能所需的图标。确保已测试新图标在各种环境中的正确显示。

</details>
<details>
<summary>packages/file-tree-next/src/browser/file-tree-contribution.ts (1)</summary>

`447-453`: **增强了文件定位命令的用户体验**

这段新代码确保在执行文件定位命令时，如果资源管理器面板未显示，则会自动激活并展开它。这是一个不错的用户体验改进，尤其是当用户从聊天界面点击文件或图片引用时非常有用。

200毫秒的延迟给UI足够的时间来更新视图状态，确保后续的文件展示操作能正确执行。

</details>
<details>
<summary>packages/i18n/src/common/zh-CN.lang.ts (2)</summary>

`1221-1221`: **聊天输入提示文本已更新**

将提示文本从"可以问我任何问题，或键入主题 '/'" 更改为"可以问我任何问题，输入 @ 可引用内容"，以反映新的文件和文件夹引用功能。这使用户更清楚如何在聊天中引用内容。

---

`1230-1231`: **新增了文件和文件夹引用的本地化文本**

添加了"当前文件"和"当前文件夹"的本地化条目，支持聊天中的文件和文件夹引用功能。这些条目将用于在聊天界面中显示引用的上下文信息。

</details>
<details>
<summary>packages/ai-native/__test__/browser/chat/chat-agent.service.test.ts (1)</summary>

`28-28`: **更新了 provideContextPrompt 为异步方法**

测试代码中的 `provideContextPrompt` 函数已从同步变为异步 (添加了 `async` 关键字)。这反映了主代码库中对应方法的更改，使其能够处理更复杂的操作，如在聊天中加载图片或文件引用时可能需要的异步文件操作。

确保所有调用此方法的代码都已经更新以处理返回的 Promise。

</details>
<details>
<summary>packages/ai-native/src/browser/components/ChatThinking.tsx (4)</summary>

`15-15`: **扩展接口以支持更多内容类型**

扩展 `ITinkingProps` 接口中的 `children` 属性类型，以支持字符串和 React 节点数组，有利于提升组件的灵活性，方便处理更复杂的内容展示，包括图片等多媒体内容。

---

`34-39`: **优化空内容判断逻辑**

新增的 `isEmptyChildren` 变量使用 useMemo 缓存判断结果，并针对数组类型做了特殊处理。这种实现方式比直接判断 `children` 更加健壮，可以正确处理各种 React 节点类型（包括图片内容）的空值检查。

---

`42-42`: **使用优化后的空值检查**

将条件判断从直接检查 `children` 改为使用 `isEmptyChildren`，确保了组件能够一致地处理各种类型的内容，包括新增支持的图片内容。

---

`61-61`: **保持进度条渲染逻辑一致性**

将进度条的条件渲染也更新为使用 `isEmptyChildren`，确保了组件在处理不同类型内容（包括图片）时的行为一致性。

</details>
<details>
<summary>packages/ai-native/src/browser/model/msg-history-manager.ts (1)</summary>

`65-65`: <details>
<summary>❓ Verification inconclusive</summary>

**消息历史管理器支持图片属性**

将 `images` 属性添加到用户消息的必需属性中，确保了消息历史可以正确记录和管理带有图片的消息。这是实现聊天中添加图片功能的关键部分之一。

不过，可能需要考虑以下几点：

1. 是否需要在 `addAssistantMessage` 和 `updateAssistantMessage` 方法中也添加对 images 的支持？
2. 确保 `toJSON` 方法能够正确序列化和反序列化包含图片的消息

---

🏁 Script executed:

```shell
#!/bin/bash
# 检查是否有其他地方使用了 images 属性但未更新

# 搜索使用 addUserMessage 方法的地方，检查它们是否已更新以提供 images 参数
rg -A 2 "addUserMessage\(" --type typescript
```

Length of output: 81

---

**请确认消息方法对 images 属性的支持一致性**

目前在文件 `packages/ai-native/src/browser/model/msg-history-manager.ts` 中，已经将 `images` 属性添加到用户消息的必需项中，确保带图片消息的正确记录。请注意以下几点：
- 检查 `addAssistantMessage` 和 `updateAssistantMessage` 方法是否也需要支持传递 `images` 参数。
- 确保 `toJSON` 方法能正确序列化和反序列化包含 `images` 的消息数据。

请运行上面的新脚本确认相关调用是否更新完毕，或手动检查这些方法，确保整体功能一致正确。

---

🏁 Script executed:

```shell

#!/bin/bash
# 搜索用户消息调用（addUserMessage）的地方，检查是否传递 images 属性
rg -A 2 "addUserMessage\(" -g "*.ts"

# 搜索助手消息新增方法（addAssistantMessage）的调用，检查是否传递 images 属性
rg -A 2 "addAssistantMessage\(" -g "*.ts"

# 搜索助手消息更新方法（updateAssistantMessage）的调用，检查是否传递 images 属性
rg -A 2 "updateAssistantMessage\(" -g "*.ts"
```

Length of output: 1206

---



</details>

**请确认图片消息支持的一致性**

经过脚本验证，当前代码在 `packages/ai-native/src/browser/model/msg-history-manager.ts` 文件中：

- 在 `addUserMessage` 方法中，已通过必需属性内包含 `images` （第65行处）确保用户消息记录支持图片。
- 但在 `addAssistantMessage` 和 `updateAssistantMessage` 方法中，函数签名依然维持为 `Omit<IExcludeMessage, 'role'>`，未显示添加对 `images` 属性的更新。

请开发者：

- 确认是否需要在 `addAssistantMessage` 与 `updateAssistantMessage` 方法中也明确支持或传递 `images` 属性。
- 检查 `toJSON` 方法，确保其能正确序列化和反序列化包含图片属性的消息。

</details>
<details>
<summary>packages/ai-native/src/browser/chat/chat.internal.service.ts (1)</summary>

`65-66`: **很好的修改，支持了聊天中的图片功能**

方法签名的变更符合PR的目标，允许在创建请求时传递图片数据。实现简单明了，将图片数组作为可选参数传递给底层服务。

</details>
<details>
<summary>packages/ai-native/src/browser/index.ts (2)</summary>

`49-52`: **添加图片上传提供者的导入**

正确引入了图片上传相关的类和令牌，为图片上传功能提供必要的类型定义。

---

`193-196`: **正确注册图片上传提供者**

将图片上传提供者注册到依赖注入系统中，确保其可被其他组件正确访问和使用。遵循了代码库中的标准服务注册模式。

</details>
<details>
<summary>packages/i18n/src/common/en-US.lang.ts (2)</summary>

`1452-1452`: **更新聊天输入框提示文本**

将提示文本从"Ask Copilot or type / for commands"改为"Ask anything, @ to mention"，清晰地指导用户可以使用@符号来提及或引用内容，这与新增的图片上传功能交互模式一致。

---

`1462-1463`: **添加上下文菜单本地化文本**

新增了"Current File"和"Current Folder"的本地化字符串，这些字符串很可能用于在使用@提及功能时，显示当前文件和当前文件夹的选项标签，完善了图片和文件上传功能的用户体验。

</details>
<details>
<summary>packages/file-search/src/node/file-search.service.ts (3)</summary>

`43-43`: **添加仅文件夹搜索选项**

添加了`onlyFolders`选项，默认为`false`，为文件搜索功能增加了过滤目录的能力，这对于支持图片上传时的文件夹浏览功能很有用。

---

`97-107`: **实现文件夹过滤逻辑**

通过使用`fs.statSync`检查文件是否为目录，正确实现了文件夹过滤功能。代码也适当处理了可能出现的异常情况。

---

`134-134`: **修复了路径深度比较的错误**

将`Path.pathDepth(a)`更正为`Path.pathDepth(b)`，修复了排序逻辑中的错误。这个修复看起来与主要功能无直接关系，但增强了代码的正确性。

</details>
<details>
<summary>packages/ai-native/src/common/index.ts (3)</summary>

`55-58`: **增加图片支持到消息结构中**

为 `IChatMessageStructure` 接口添加了可选的 `images` 属性，这是实现图片聊天功能的基础。接口定义清晰，并提供了适当的 JSDoc 注释说明。

---

`207-207`: **代理请求中添加图片支持**

在 `IChatAgentRequest` 接口中添加了 `images` 可选属性，与 `IChatMessageStructure` 接口保持一致，确保图片数据可以在请求处理流程中传递。

---

`245-245`: **请求消息添加图片属性**

在 `IChatRequestMessage` 接口中添加了 `images` 可选属性，完成了图片数据在所有相关接口中的一致性支持。

</details>
<details>
<summary>packages/ai-native/src/browser/chat/chat-manager.service.ts (3)</summary>

`153-153`: **更新方法签名以支持图片参数**

`createRequest` 方法签名已更新，新增了可选的 `images` 参数，与接口变更保持一致。

---

`163-163`: **将图片数据传递给请求对象**

将 `images` 参数正确地传递到 `model.addRequest` 方法中，确保图片数据被包含在创建的请求对象中。

---

`194-194`: **在请求属性中包含图片数据**

将 `request.message.images` 添加到 `requestProps` 对象中，确保图片数据能够被正确传递给代理服务进行处理。

</details>
<details>
<summary>packages/ai-native/src/browser/components/mention-input/mention-input.module.less (3)</summary>

`1-34`: **整体布局规则明确，易于维护**  
`.input_container` 及其子元素的基础布局清晰，但需要注意 `.model_selector`、`.editor_area` 与 `.editor` 等内部元素在高度受限时的自适应表现，防止文本或控件被裁剪。

---

`75-132`: **`.footer` 与 `.send_button` 的交互逻辑可读性较高**  
使用统一的悬浮与点击反馈能提升一致性；建议确保 `transition` 动画效果在低配置设备或低帧率情况下，也能保持流畅，避免卡顿。

---

`192-334`: **下拉列表、空状态与加载状态均有独立样式，设计完善**  
加载动画及空状态提示能够有效提升用户体验，但仍需定期检查颜色或字体，以适配更多主题场景。

</details>
<details>
<summary>packages/ai-native/src/browser/components/mention-input/mention-input.tsx (1)</summary>

`90-159`: **二级菜单的防抖与异步加载实现避免了多余请求**  
现有逻辑对高优先级项目的即时展示与异步加载的合并思路清晰，但要留意可能存在竞争条件（例如组件卸载后或快速切换筛选输入）的情况，需确保异步请求返回时不会导致更新已卸载的组件。

</details>
<details>
<summary>packages/ai-native/src/browser/ai-core.contribution.ts (4)</summary>

`98-98`: **引入新依赖模块的导入语句。**  
此处引入 `ImageUploadProviderRegistryToken` 用于图像上传功能的注册，路径看起来合理，未发现命名冲突或循环依赖。

---

`118-118`: **新增的 `IImageUploadProviderRegistry` 类型导入。**  
导入定义清晰，与周边接口风格一致。暂未发现命名或引用问题。

---

`201-203`: **在类中使用依赖注入注入 `imageUploadProviderRegistry`。**  
此处通过 `@Autowired` 注解实现构造注入，字段命名与接口名相符，易于理解和维护。

---

`461-461`: **在 `registerFeature` 方法中注册图像上传功能。**  
使用可选链调用 `contribution.registerImageUploadProvider?.(this.imageUploadProviderRegistry)`，确保在不存在实现时不会抛出异常。整体逻辑无明显问题。

</details>
<details>
<summary>packages/ai-native/src/browser/chat/chat-agent.service.ts (3)</summary>

`62-62`: **将 `llmContextService` 注入为受保护属性。**  
从命名上看更能体现其与 LLM（大语言模型）相关的用途，增强可读性。

---

`155-155`: **在首次发送消息时为请求追加上下文。**  
通过 `await this.provideContextMessage(...)` 将上下文融合进 `request.message`，逻辑清晰。留意上下文为空或异常情况时对提示的影响。

---

`157-157`: **在部分条件下重新获取上下文信息。**  
当需要 regenerate 或聊天历史为空时，也会执行等待上下文处理。应确保对 `shouldUpdateContext` 的赋值逻辑足够准确，避免重复或漏掉更新。

</details>
<details>
<summary>packages/ai-native/src/browser/components/ChatMentionInput.tsx (3)</summary>

`1-23`: **导入与基础模块设置。**  
- 包含了 React 相关依赖与 IDE 核心服务，如 `LabelService`、`CommandService` 等。  
- 使用 `styles` 从 `.less` 文件导入样式，符合组件化的做法。  
整体无显著问题，命名清晰，依赖结构合理。

---

`51-314`: **核心组件 `ChatMentionInput` 逻辑。**  
1. **状态管理**  
   - 使用 `useState` 管理输入框内容 `value` 和上传图片数组 `images`，简单明了。  
   - 通过 `useEffect` 同步更新外部传入的 `props.value`，防止数据源不一致。  
2. **默认菜单项**  
   - 将文件和文件夹分别封装为提及项 (`MentionType.FILE` / `MentionType.FOLDER`)，并提供获取最高级及搜索结果的回调函数。  
   - 依赖 `workspaceService`、`searchService`、`recentFilesManager` 等服务，逻辑清晰。  
3. **操作按钮与配置**  
   - `defaultMentionInputFooterOptions` 中自定义按钮如 `MCP Server` 打开相应命令，UI 扩展方式灵活。  
4. **发送及停止功能**  
   - `handleSend` 在被调用时会携带 `images`，实现文本和图片一起发送，满足多媒体需求。  
   - `handleStop` 通过调用 `aiChatService.cancelRequest();` 终止当前请求。  
5. **图片上传处理**  
   - `handleImageUpload` 中先获取 `imageUploadProvider`，若不存在则提示错误；已考虑用户体验。  
   - 上传成功后使用 `setImages` 更新组件内图片列表。  
整体结构层次分明，逻辑易读，无明显性能隐患。

---

`316-335`: **子组件 `ImagePreviewer` 预览并管理已上传图片。**  
- 简单直观地在缩略图区域显示图片，并提供删除按钮。  
- 采用 `index` 作为 key，对小规模图片数组而言基本可行。若后续需要处理图片顺序变化，需进一步优化 key 生成方式。  
- 视觉与交互逻辑合理，删除功能通过回调向父组件传递。

</details>
<details>
<summary>packages/ai-native/src/common/prompts/context-prompt-provider.ts (12)</summary>

`3-3`: **导入工作区服务用于获取文件路径，逻辑合理。**

---

`14-14`: **方法签名改为异步以返回 `Promise<string>`，适配新的异步需求，没问题。**

---

`22-24`: **注入 `workspaceService` 便于后续获取工作区路径，使用方式恰当。**

---

`25-35`: **在 `provideContextPrompt` 中异步获取当前文件信息并构建模板，结构清晰，易于维护。**

---

`37-53`: **`getCurrentFileInfo` 通过 URI 转相对路径后返回文件内容和语言信息，逻辑简明且符合使用场景。**

---

`55-82`: **`buildPromptTemplate` 将所有上下文片段组装到一个多段字符串里，结构清楚且易于扩展。**

---

`84-92`: **`buildRecentFilesSection` 用来构造 `<recently_viewed_files>` 部分，逻辑简单明了。**

---

`94-112`: **`buildAttachedFilesSection` 依次创建文件块与错误信息区块，清晰易读。**

---

`114-120`: **`buildFileContentSection` 以三引号格式输出文件内容，符合 Markdown 语法，没问题。**

---

`122-128`: **`buildLineErrorsSection` 用 `<linter_errors>` 标签包裹错误文本，形式统一且易于识别。**

---

`130-136`: **`buildAttachedFoldersSection` 以 `<attached_folders>` 包裹文件夹信息，逻辑与文件处理一致。**

---

`138-147`: **`buildCurrentFileSection` 将当前文件的语言、高亮内容结构化输出，易于后续解析。**

</details>
<details>
<summary>packages/theme/src/common/color-tokens/chatColors.ts (1)</summary>

`1-63`: **新增多组聊天相关色彩令牌，涵盖了边框、背景和前景等多种场景，定义完备且便于主题管理。**

</details>
<details>
<summary>packages/ai-native/src/browser/components/ChatEditor.tsx (12)</summary>

`5-14`: **新增对多种服务与方法的导入，如 `EDITOR_COMMANDS`、`FILE_COMMANDS`、`LabelService` 等，符合后续操作需求。**

---

`20-22`: **导入 `CommandService` 与 `URI`，为后续命令执行和路径解析提供支持。**

---

`37-37`: **引入 `IWorkspaceService` 的类型声明，便于在后文使用工作区相关方法。**

---

`172-173`: **新增正则 `rgAttachedFile` 和 `rgAttachedFolder`，用于匹配文件与文件夹标签，思路清晰。**

---

`174-194`: **`handleAttachmentClick` 根据工作区根路径依次尝试执行命令，如出错则继续下一个根路径；`continue` 用于跳过后续逻辑并非多余，可忽略静态分析提示。**

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 189-189: Unnecessary continue statement

Unsafe fix: Delete the unnecessary continue statement


(lint/correctness/noUnnecessaryContinue)

</details>

</details>

---

`195-204`: **`renderAttachment` 动态渲染点击区域和对应图标，便于用户快速打开文件或文件夹。**

---

`249-289`: **在处理内联与代码块时，增添对 `<attached_file>` 与 `<attached_folder>` 的检测与解析，内容拆分与片段插入逻辑较完善。**

---

`308-310`: **在 `CodeBlockWrapper` Props 中增加注入 `labelService`、`commandService` 和 `workspaceService`，与子组件处理保持一致。**

---

`322-330`: **将外部服务与数据透传给 `CodeBlock`，简化组件间依赖注入，写法合理。**

---

`337-337`: **在 `CodeBlockWrapperInput` 中增加 `images`、`labelService` 等 Props，扩展功能以支持图片显示和命令操作。**



Also applies to: 341-343

---

`373-377`: **使用 `images?.map` 动态渲染上传的图片，实现聊天时的图片可视化功能。**

---

`390-398`: **为最终渲染插入了 `CodeBlock`，同时将命令与工作区服务一并传递，避免重复注入。**

</details>
<details>
<summary>packages/ai-native/src/browser/chat/chat.view.tsx (4)</summary>

`4-11`: **新增依赖注入的导入项看起来正确。**  
这些服务被正确注入，对于增强聊天功能的可扩展性很有帮助，目前未发现问题。

---

`40-40`: **注入 `IWorkspaceService` 合理。**  
工作区服务对获取相对/绝对路径等功能有帮助，理论上无冲突。

---

`43-43`: **`LLMContextService` 的引入符合上下文需求。**  
这在后续处理文件/文件夹上下文时会非常实用，暂无问题。

---

`606-610`: **`handleAgentReply` 中对 `images` 属性的支持完善了流程。**  
继续保持与上层数据结构的同步，不然可能导致数据不一致。

</details>
<details>
<summary>packages/ai-native/src/common/llm-context.ts (3)</summary>

`1-1`: **从 `ai` 模块引入 `DataContent`**  
仅是基础类型导入，功能方面暂不影响。

---

`21-25`: **`addFolderToContext` 为上下文添加文件夹的功能与当前需求匹配。**  
请确认后续若文件夹过大，是否需要可视化或分页管理，避免上下文递归深度过大。

---

`43-43`: **序列化改为返回 `Promise<SerializedContext>` 符合异步场景。**  
必须确保调用方知道这是异步操作，不要在逻辑里产生阻塞。

</details>
<details>
<summary>packages/ai-native/src/browser/types.ts (2)</summary>

`1-1`: **新增 `import { DataContent } from 'ai'`**  
仅是类型/数据结构的引入，可以简化对图片内容的通用处理。

---

`347-350`: **`registerImageUploadProvider` 为核心贡献点添加图片上传能力。**  
在多模块协作时，需要保证注册逻辑简单、调用方易于使用。

</details>
<details>
<summary>packages/ai-native/src/node/base-language-model.ts (4)</summary>

`1-10`: **导入的模块支持图片消息功能**

导入了 `CoreUserMessage`、`ImagePart` 和 `TextPart` 类型，这些是支持图片消息所必需的。这些导入与 PR 的目标（支持在聊天中添加图片）一致。

---

`60-60`: **正确传递图片参数**

在 `request` 方法中传递了 `options.images` 参数给 `handleStreamingRequest` 方法，这是实现图片支持功能的关键部分。

---

`90-90`: **添加图片参数声明**

为 `handleStreamingRequest` 方法添加了 `images?: string[]` 可选参数，类型声明正确，保持了与调用者接口的一致性。

---

`103-114`: **实现了图片和文本消息的组合逻辑**

代码能够正确处理包含图片的消息情况。当存在图片时，将内容构造为包含 `TextPart` 和 `ImagePart` 的数组；当没有图片时，保持原有的纯文本行为。这种条件处理方式保证了向后兼容性。

</details>
<details>
<summary>packages/ai-native/src/browser/components/mention-input/types.ts (5)</summary>

`1-3`: **引入了必要的依赖**

导入了 `LabelService` 和 `IWorkspaceService` 类型，用于组件的标签和工作区功能支持，是组件实现的基础。

---

`4-14`: **定义了提及项的核心接口**

`MentionItem` 接口定义了提及项的核心属性，包括 id、类型、文本等基本信息，以及获取子项的方法。接口设计合理，有良好的可扩展性。

---

`50-54`: **定义了支持文件和文件夹的提及类型**

`MentionType` 枚举定义了三种提及类型：FILE、FOLDER 和 CODE，这与 PR 目标中支持添加图片到聊天相关，因为这些类型将用于处理文件附件。

---

`71-83`: **组件接口设计支持图片上传功能**

`MentionInputProps` 接口包含了 `onImageUpload` 属性，用于处理图片上传事件，完全符合 PR 的目标。接口设计考虑了组件所需的各种交互事件和配置选项，结构清晰。

---

`78-78`: **图片上传处理函数定义**

添加了 `onImageUpload` 回调函数，用于处理图片上传事件，这是实现 PR 目标（支持在聊天中添加图片）的关键部分。

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T14:10:15Z
user: login: Ricbet
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T01:42:01Z
user: login: erha19
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-14T04:03:01Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>packages/ai-native/src/browser/chat/chat.view.tsx (1)</summary><blockquote>

`866-878`: _⚠️ Potential issue_

**注意：ChatInputWrapperRender 的 onSend 属性类型可能不匹配**

当前代码将 `handleSend`（返回 `Promise<void>`）传递给 `onSend` 属性，但根据先前的评论，这里可能存在类型不匹配问题。请确保 `ChatInputRender` 接口已更新，以避免构建失败。


根据之前的评论和构建错误，需要更新 `ChatInputRender` 接口中 `onSend` 的类型定义：

```diff
- onSend: (value: string, images?: string[], agentId?: string, command?: string) => void;
+ onSend: (value: string, images?: string[], agentId?: string, command?: string) => Promise<void>;
```

如果此问题尚未解决，将导致类型检查失败和构建错误。

</blockquote></details>

</blockquote></details>

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts (1)</summary><blockquote>

`513-518`: _⚠️ Potential issue_

**图片上传提供者实现需要改进**

当前实现将任何上传的图片都映射到一个硬编码的图片 URL，适合样例展示但不适合生产环境。

建议：
1. 实现真实的文件上传逻辑，处理不同的文件类型和大小
2. 添加错误处理机制
3. 考虑添加进度反馈



```diff
registerImageUploadProvider(registry: IImageUploadProviderRegistry): void {
  registry.registerImageUploadProvider({
-    imageUpload: async (file) =>
-      'https://idealab-platform.oss-accelerate.aliyuncs.com/20231125/33808fdb-10ad-428c-8a76-11532ad93b15_idealab2.png?Expires=4102329600&OSSAccessKeyId=LTAI5tFJF3QLwHzEmkhLs9dB&Signature=YcbZrM98pHRRMd%2BGFaI2OiFf8Z8%3D',
+    imageUpload: async (file) => {
+      try {
+        // 实际上传逻辑，例如：
+        // const formData = new FormData();
+        // formData.append('file', file);
+        // const response = await fetch('/api/upload', { method: 'POST', body: formData });
+        // const data = await response.json();
+        // return data.url;
+        
+        // 开发环境临时实现：
+        await new Promise(resolve => setTimeout(resolve, 500)); // 模拟网络延迟
+        return 'https://idealab-platform.oss-accelerate.aliyuncs.com/20231125/33808fdb-10ad-428c-8a76-11532ad93b15_idealab2.png?Expires=4102329600&OSSAccessKeyId=LTAI5tFJF3QLwHzEmkhLs9dB&Signature=YcbZrM98pHRRMd%2BGFaI2OiFf8Z8%3D';
+      } catch (error) {
+        console.error('图片上传失败', error);
+        throw new Error('图片上传失败');
+      }
+    },
  });
}
```

另外，根据过去的评论，可以考虑将此功能注册在 `IChatFeatureRegistry` 中，这样语义会更加明确，表明这个图片上传功能是服务于聊天面板的。

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>packages/components/src/image/index.tsx (1)</summary><blockquote>

`1-24`: **实现了基础的图片组件，但可以进一步优化**

这个新组件使用 `rc-image` 库实现了基础的图片显示和预览功能。实现比较简洁，但有几点建议：

1. 预览容器通过简单的选择器查找可能会导致在某些布局中出现问题
2. 组件没有处理加载失败或加载中的状态
3. 缺少对图片尺寸限制的处理，可能导致大图片显示问题

建议添加更多自定义配置选项，比如错误处理和尺寸控制。


```diff
export const Image = (props: ImageProps) => (
  <RcImage
    prefixCls='kt-image'
    preview={{
      getContainer: () => document.getElementById('main') || document.getElementsByTagName('body')?.[0],
      mask: (
        <div className='mask'>
          <Icon iconClass='codicon codicon-eye' />
        </div>
      ),
+     // 添加更多预览配置选项
+     // onVisibleChange: (visible) => { /* 处理预览状态变化 */ },
    }}
+   // 添加错误处理和加载状态
+   // fallback="data:image/png;base64,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"
    {...props}
  />
)
```

</blockquote></details>
<details>
<summary>packages/components/src/style/mixins.less (1)</summary><blockquote>

`145-172`: **新增 modal-mask mixin 但缺少响应式和暗色模式支持**

这个 mixin 很好地封装了模态框遮罩层的样式，包括动画和定位。但有几点建议：

1. 背景色使用了变量 `@modal-mask-bg`，但没有为暗色模式提供变体
2. z-index 使用了变量 `@zindex-modal-mask`，确保此变量在 variable.less 中已定义
3. 缺少响应式设计考虑，特别是在移动设备上的表现

建议添加媒体查询和暗色模式支持，以提升用户体验。


```diff
.modal-mask() {
  pointer-events: none;

  &.@{prefix}-zoom-enter,
  &.@{prefix}-zoom-appear {
    transform: none; // reset scale avoid mousePosition bug
    opacity: 0;
    animation-duration: @animation-duration-slow;
    user-select: none; // https://github.com/ant-design/ant-design/issues/11777
  }

  &-mask {
    .box(fixed);
    z-index: @zindex-modal-mask;
    height: 100%;
    background-color: @modal-mask-bg;
+    // 暗色模式支持
+    .kt-dark & {
+      background-color: @modal-mask-bg-dark;
+    }

    &-hidden {
      display: none;
    }
  }

  &-wrap {
    .box(fixed);
    overflow: auto;
    outline: 0;
+    // 移动设备适配
+    @media (max-width: @screen-xs) {
+      padding: 0;
+    }
  }
}
```

</blockquote></details>
<details>
<summary>packages/ai-native/src/browser/components/ChatMentionInput.tsx (1)</summary><blockquote>

`301-318`: **建议增强图片上传处理功能**

图片上传处理函数有几处可以改进：

1. 缺少文件大小限制，可能导致大文件上传性能问题
2. 上传过程缺少错误处理，如果 `imageUploadProvider.imageUpload(file)` 调用失败，可能导致程序崩溃
3. 允许的文件类型硬编码在函数中，建议移至常量或配置中


建议进行以下修改：

```diff
+ const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
+ const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];

const handleImageUpload = useCallback(
  async (file: File) => {
-    const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
-    if (!allowedTypes.includes(file.type)) {
+    if (!ALLOWED_IMAGE_TYPES.includes(file.type)) {
      messageService.error('Only JPG, PNG, WebP and GIF images are supported');
      return;
    }
+    
+    if (file.size > MAX_FILE_SIZE) {
+      messageService.error(`File size exceeds ${MAX_FILE_SIZE / (1024 * 1024)}MB limit`);
+      return;
+    }

    const imageUploadProvider = imageUploadProviderRegistry.getImageUploadProvider();
    if (!imageUploadProvider) {
      messageService.error('No image upload provider found');
      return;
    }
-    const data = await imageUploadProvider.imageUpload(file);
-    setImages([...images, data]);
+    try {
+      const data = await imageUploadProvider.imageUpload(file);
+      setImages([...images, data]);
+    } catch (error) {
+      messageService.error(`Failed to upload image: ${error.message || 'Unknown error'}`);
+    }
  },
  [images],
);
```

</blockquote></details>
<details>
<summary>packages/components/src/image/styles.less (1)</summary><blockquote>

`108-157`: **操作控件样式定义完整**

操作区域样式设计合理，包括禁用状态、悬停效果等。注意到操作包装器被设置为 `display: none`，说明部分功能可能为将来准备。进度条位置使用了绝对定位和 transform 实现了良好的居中效果。

注释掉的代码表明这部分功能可能正在开发中。建议在注释中添加更多上下文，说明这些样式将在何时启用及其预期功能。例如：

```diff
- // 暂时不需要控制
+ // 暂时不需要显示操作控件，将在实现高级图片编辑功能时启用
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 3fd08bebe9118cc00b348a26f329b6af5e6c13fb and 2584d6f1371f06632705ffcc8dbf861182b84732.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `yarn.lock` is excluded by `!**/yarn.lock`, `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (18)</summary>

* `packages/ai-native/src/browser/chat/chat.view.tsx` (9 hunks)
* `packages/ai-native/src/browser/components/ChatEditor.tsx` (4 hunks)
* `packages/ai-native/src/browser/components/ChatInput.tsx` (2 hunks)
* `packages/ai-native/src/browser/components/ChatMentionInput.tsx` (6 hunks)
* `packages/ai-native/src/browser/components/components.module.less` (1 hunks)
* `packages/ai-native/src/browser/components/mention-input/mention-input.module.less` (1 hunks)
* `packages/ai-native/src/browser/components/mention-input/mention-input.tsx` (4 hunks)
* `packages/ai-native/src/browser/components/mention-input/types.ts` (2 hunks)
* `packages/ai-native/src/browser/context/llm-context.service.ts` (1 hunks)
* `packages/ai-native/src/common/index.ts` (3 hunks)
* `packages/ai-native/src/node/base-language-model.ts` (5 hunks)
* `packages/components/package.json` (1 hunks)
* `packages/components/src/image/index.tsx` (1 hunks)
* `packages/components/src/image/styles.less` (1 hunks)
* `packages/components/src/style/mixins.less` (1 hunks)
* `packages/components/src/style/variable.less` (3 hunks)
* `packages/file-tree-next/src/browser/file-tree-contribution.ts` (1 hunks)
* `packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts` (2 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* packages/ai-native/src/browser/context/llm-context.service.ts

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (7)</summary>

* packages/ai-native/src/browser/components/ChatEditor.tsx
* packages/ai-native/src/browser/components/mention-input/mention-input.module.less
* packages/file-tree-next/src/browser/file-tree-contribution.ts
* packages/ai-native/src/browser/components/mention-input/types.ts
* packages/ai-native/src/common/index.ts
* packages/ai-native/src/browser/components/mention-input/mention-input.tsx
* packages/ai-native/src/node/base-language-model.ts

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (8)</summary>

* GitHub Check: build (ubuntu-latest, 20.x)
* GitHub Check: build (macos-latest, 20.x)
* GitHub Check: build-windows
* GitHub Check: unittest (ubuntu-latest, 18.x, jsdom)
* GitHub Check: unittest (ubuntu-latest, 18.x, node)
* GitHub Check: unittest (macos-latest, 18.x, jsdom)
* GitHub Check: unittest (macos-latest, 18.x, node)
* GitHub Check: ubuntu-latest, Node.js 20.x

</details>

<details>
<summary>🔇 Additional comments (30)</summary><blockquote>

<details>
<summary>packages/components/package.json (1)</summary>

`29-29`: **添加了rc-image依赖，但版本控制策略需要考虑**

引入了rc-image依赖用于实现图片预览功能，当前使用的版本为^7.11.0。

建议考虑：
1. 确保版本兼容性，特别是与其他RC组件的版本协调
2. 评估是否需要将依赖锁定到特定版本而不是使用^符号允许次要版本更新

</details>
<details>
<summary>packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts (1)</summary>

`16-16`: **API导入顺序调整**

在保持排序规则一致性的基础上添加了`IImageUploadProviderRegistry`接口的导入。

</details>
<details>
<summary>packages/components/src/style/mixins.less (1)</summary>

`137-143`: **添加了通用的盒模型定位 mixin**

新增的 `.box()` mixin 提供了一种快捷方式来设置元素的绝对/固定定位并填充其容器，代码简洁有效。

这是一个良好的抽象，可以在多个组件中重用，减少重复代码。

</details>
<details>
<summary>packages/ai-native/src/browser/components/ChatInput.tsx (2)</summary>

`164-165`: **方法签名更新准确**

接口中的 `onSend` 方法签名已正确更新，包含了可选的 `images` 参数，与新功能要求一致。

---

`343-345`: **正确传递图片参数**

`handleSendLogic` 函数已正确更新以传递空图片数组作为默认值，确保与更新后的接口兼容。

</details>
<details>
<summary>packages/ai-native/src/browser/components/components.module.less (2)</summary>

`605-646`: **缩略图样式实现良好**

新增的缩略图容器和相关组件的样式结构合理，视觉效果一致。删除按钮的hover效果增强了用户体验。

---

`648-650`: **图片包装器样式简洁明了**

`.image_wrapper` 类定义简单明了，符合当前样式指南。

</details>
<details>
<summary>packages/components/src/style/variable.less (3)</summary>

`62-62`: **z-index变量设置合理**

新增的 `@zindex-image` 变量值为1080，在现有z-index体系中位置合适，不会导致层级冲突。

---

`116-123`: **新增间距变量定义清晰**

新增的 `@padding-xss` 和 `@margin-xss` 变量为样式系统增加了更细粒度的间距控制，定义合理。

---

`218-226`: **图片相关变量定义完善**

新增的图片相关变量定义全面，包括尺寸、颜色、字体大小等关键属性，并正确引用了现有颜色变量，保持了样式系统的一致性。

</details>
<details>
<summary>packages/ai-native/src/browser/components/ChatMentionInput.tsx (6)</summary>

`24-31`: **接口更新符合需求**

`IChatMentionInputProps` 接口中的 `onSend` 方法签名已正确更新，添加了图片参数支持。

---

`40-40`: **新增图片属性类型定义合理**

为 `IChatMentionInputProps` 接口添加了 `images` 属性，类型为 `Array<DataContent | URL>`，定义合理。

---

`56-56`: **正确初始化图片状态**

组件正确初始化了图片状态，并从 props 中获取初始值。

---

`257-274`: **上传图片按钮实现良好**

上传图片按钮的实现简洁有效，使用了原生文件选择对话框，并正确处理了文件选择事件。

---

`290-296`: **发送时正确包含图片数据**

`handleSend` 方法正确地将图片数据包含在发送请求中，并将图片 URL 转换为字符串。

---

`345-364`: **预览组件实现良好**

`ImagePreviewer` 组件实现简洁明了，正确使用了现有的样式类和组件。删除功能实现正确，通过索引删除对应图片。

</details>
<details>
<summary>packages/ai-native/src/browser/chat/chat.view.tsx (9)</summary>

`284-284`: **已更新 `handleSend` 调用以支持图片参数**

函数调用已正确更新，现在包含了 `message.images` 参数，与新的函数签名保持一致。

---

`465-474`: **函数签名已正确更新以支持图片参数**

`renderUserMessage` 函数的签名已适当扩展，增加了对 `images?: string[]` 参数的支持，并在解构赋值中正确引用了该参数。这个更改与 PR 目标（支持在聊天中添加图片）完全一致。

---

`483-483`: **适当地向 ChatUserRoleRender 组件传递了图片数据**

组件属性已更新，确保图片数据能够传递到渲染组件，保持了数据流的完整性。

---

`489-489`: **CodeBlockWrapperInput 组件现在支持图片属性**

已正确更新组件属性以包含图片数据，确保在不使用自定义渲染器的情况下也能显示图片。

---

`608-611`: **handleAgentReply 函数已更新以处理图片数据**

函数参数和 `createRequest` 调用已正确更新，确保图片数据能够正确传递到 AI 聊天服务。此更改符合功能需求，使得聊天请求可以包含图片数据。

---

`637-637`: **确保在历史记录中保存图片数据**

消息历史管理器的 `addUserMessage` 调用已更新，添加了 `images: images || []` 属性，确保即使没有提供图片也能使用空数组作为默认值。这样可以保持数据结构的一致性，避免潜在的 null/undefined 错误。

---

`646-646`: **向用户消息渲染器传递图片数据**

在调用 `renderUserMessage` 时正确传递了图片参数，保持了数据流的连贯性。

---

`762-762`: **恢复功能现已支持图片数据**

在恢复历史会话时正确传递图片数据，确保用户消息的完整性。这对于保持会话状态的完整性非常重要。

---

`679-720`: <details>
<summary>❓ Verification inconclusive</summary>

**handleSend 函数已适当更新以支持图片上传功能**

函数签名和实现已更新，现在支持接收和处理图片数据。文件和文件夹占位符处理逻辑保持不变，并在最后正确地将处理后的消息和图片数据传递给 `handleAgentReply`。

不过，需要注意以下几点：

1. `onSend` 的异步返回类型可能与接口定义不匹配，需确保 `ChatInputRender` 接口也已更新。
2. 多个文件/文件夹占位符的处理可能在大量数据时影响性能。


请检查 `ChatInputRender` 接口的定义是否已更新为返回 `Promise<void>`，以避免类型不匹配错误：

---

🏁 Script executed:

```shell
#!/bin/bash
# 查找 ChatInputRender 接口定义
rg -A 3 "interface.*ChatInputRender" --type=typescript
```

Length of output: 91

---



</details>

**更新建议：请确认 ChatInputRender 接口的返回类型**

handleSend 函数现已更新，支持图片上传，并保持对文件和文件夹占位符的处理逻辑，最终将处理后的内容与图片数据传递给 handleAgentReply。但请注意：

- 请检查 ChatInputRender 接口中 onSend 方法的返回类型是否已更新为 `Promise<void>`，以避免类型不匹配问题。（由于之前用于定位该接口定义的脚本因文件类型识别问题未返回结果，请手动验证或采用类似以下命令重新检查：  
  ```shell
  rg -A 3 "interface.*ChatInputRender" -g "*.{ts,tsx}"
  ```
  ）
- 注意：在处理多个文件/文件夹占位符时，如数据量较大，可能存在性能隐患，建议进一步评估。

</details>
<details>
<summary>packages/components/src/image/styles.less (5)</summary>

`1-6`: **样式文件结构组织良好**

文件开始部分导入了必要的变量和混合器，并定义了清晰的类名前缀，这有助于保持命名一致性和避免样式冲突。

---

`7-24`: **图片容器和基本样式定义完善**

图片容器使用了相对定位，便于后续元素的绝对定位。图片样式设置合理，特别是占位符的背景图片使用了 base64 编码的 SVG，减少了额外的网络请求。

---

`25-58`: **遮罩层效果实现优雅**

遮罩层实现了良好的交互效果，使用透明度过渡动画提升了用户体验。文本信息样式设置合理，处理了文本溢出的情况。占位符使用了 `.box()` 混合器保持了代码的简洁性。

---

`59-107`: **预览功能实现全面且交互流畅**

图片预览部分设计全面，包含了图片缩放、光标样式变化等交互细节。尤其是使用了 `scale3d` 和 transition 实现平滑的缩放效果，以及针对拖动状态的特殊处理。

---

`158-205`: **切换按钮实现完整且考虑了各种状态**

左右切换按钮的样式定义全面，包括位置、大小、交互效果和禁用状态。使用固定定位确保了按钮在滚动时仍然可见，增强了用户体验。

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T04:05:27Z
user: login: erha19
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-14T06:02:08Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>packages/ai-native/src/browser/components/mention-input/mention-input.tsx (2)</summary><blockquote>

`280-292`: **图片粘贴处理逻辑实现完整且安全**

该函数正确地实现了从剪贴板检测和提取图片文件的功能，并在找到图片时阻止默认粘贴行为，转而调用提供的 `onImageUpload` 回调。

代码注意事项：
- 代码中使用了 ESLint 禁用注释来允许传统 for 循环，建议考虑使用 `for...of` 循环以符合项目代码风格
- 防御性编程做得很好，确保了 `file` 存在且 `onImageUpload` 已定义才继续处理


您可以考虑将循环改为更现代的 `for...of` 语法，如下:

```diff
- // eslint-disable-next-line @typescript-eslint/prefer-for-of
- for (let i = 0; i < items.length; i++) {
-   if (items[i].kind === 'file' && items[i].type.startsWith('image/')) {
-     const file = items[i].getAsFile();
+ for (const item of Array.from(items)) {
+   if (item.kind === 'file' && item.type.startsWith('image/')) {
+     const file = item.getAsFile();
      if (file && onImageUpload) {
        e.preventDefault();
        onImageUpload(file);
      }
    }
  }
```

---

`280-292`: **建议增加针对图片上传功能的单元测试**

新增的图片上传功能缺少相应的单元测试，为确保功能稳定性和防止未来回归，建议添加测试用例。

测试应覆盖以下场景：
- 粘贴不同格式的图片文件
- 粘贴大尺寸图片
- 粘贴非图片内容时的行为
- 未提供 `onImageUpload` 回调时的行为



需要我提供单元测试示例代码来帮助实现这些测试用例吗？

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b74b77250d640503da31ed023efdb2dcb62b1aae and 893951c75d6bf84ef252369a8ee3a3040d57709d.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `packages/ai-native/src/browser/components/mention-input/mention-input.tsx` (4 hunks)
* `packages/components/src/index.ts` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (8)</summary>

* GitHub Check: unittest (ubuntu-latest, 18.x, jsdom)
* GitHub Check: unittest (ubuntu-latest, 18.x, node)
* GitHub Check: build-windows
* GitHub Check: unittest (macos-latest, 18.x, jsdom)
* GitHub Check: build (ubuntu-latest, 20.x)
* GitHub Check: unittest (macos-latest, 18.x, node)
* GitHub Check: build (macos-latest, 20.x)
* GitHub Check: ubuntu-latest, Node.js 20.x

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>packages/components/src/index.ts (1)</summary>

`12-12`: **模块导出新增成功添加**

代码变更清晰地添加了对 `image` 模块的导出，符合现有的导出模式，这允许该模块在整个应用中被访问和使用，为新增的图片上传功能提供了必要的基础支持。

</details>
<details>
<summary>packages/ai-native/src/browser/components/mention-input/mention-input.tsx (3)</summary>

`21-21`: **新增 `onImageUpload` 属性以支持图片上传功能**

通过在 `MentionInputProps` 接口中添加 `onImageUpload` 属性，组件现在能够接收和处理图片上传的回调函数，这是实现图片上传到聊天功能的关键一步。

---

`920-920`: **成功集成粘贴事件处理器**

`onPaste` 事件处理器已正确绑定到可编辑的 div 元素上，确保了当用户粘贴图片内容时可以触发 `handlePaste` 函数。这完成了图片粘贴功能的事件链接，是功能正常工作的必要条件。

---

`886-886`: **图标渲染灵活性增强**

修改后的代码通过条件判断同时支持了两种图标显示方式：使用 `getIcon(button.icon)` 或直接使用 `button.iconClass`，提高了组件的扩展性和适应性。

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T06:45:55Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts (2)</summary><blockquote>

`513-520`: **改进图片上传提供者实现**

当前实现直接将图片转为base64并返回，但缺少类型验证和详细的错误处理。对于实际生产环境，这种实现可能存在问题：

1. 缺少图片类型验证（如确保仅接受jpeg、png等常见图片格式）
2. base64编码会使数据大小增加约33%，这对于接近大小限制的图片可能导致意外错误
3. 没有完整的错误处理机制（上传失败的详细提示）


建议改进实现如下：

```diff
registerImageUploadProvider(registry: IImageUploadProviderRegistry): void {
  registry.registerImageUploadProvider({
    imageUpload: async (file) => {
+     const validImageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
+     
+     if (!validImageTypes.includes(file.type)) {
+       throw new Error('不支持的图片格式，请上传JPG、PNG、GIF或WEBP格式');
+     }
+     
+     try {
        const base64 = await imageToBase64(file);
        return new URL(base64);
+     } catch (error) {
+       console.error('图片上传处理失败', error);
+       throw new Error(`图片上传失败: ${error.message}`);
+     }
    },
  });
}
```

---

`523-541`: **改进图片文件处理逻辑**

当前的`imageToBase64`函数已经实现了基本的大小限制检查，但仍有改进空间：

1. 可以添加图片类型验证
2. base64编码对于大型图片可能导致性能问题
3. 考虑添加进度反馈机制以提升用户体验


建议如下改进：

```diff
const MAX_IMAGE_SIZE = 3 * 1024 * 1024;

const imageToBase64 = (file: File) =>
  new Promise<string>((resolve, reject) => {
    if (file.size > MAX_IMAGE_SIZE) {
-     reject(new Error('Image size exceeds 3MB limit'));
+     reject(new Error('图片大小超过3MB限制'));
      return;
    }
+    
+   const validImageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
+   if (!validImageTypes.includes(file.type)) {
+     reject(new Error('不支持的图片格式，请上传JPG、PNG、GIF或WEBP格式'));
+     return;
+   }

    const reader = new FileReader();
    reader.onload = () => {
      const base64String = reader.result as string;
      resolve(base64String);
    };
    reader.onerror = () => {
-     reject(new Error('Failed to convert image to base64'));
+     reject(new Error('图片转换失败'));
    };
+   // 可选：添加进度反馈
+   reader.onprogress = (event) => {
+     if (event.lengthComputable) {
+       const progress = Math.round((event.loaded / event.total) * 100);
+       console.log(`图片转换进度: ${progress}%`);
+       // 这里可以触发进度更新事件
+     }
+   };
    reader.readAsDataURL(file);
  });
```

长期来看，对于生产环境，应考虑替代base64的解决方案，如使用实际的文件上传服务，避免在聊天消息中嵌入过大的base64数据。

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 893951c75d6bf84ef252369a8ee3a3040d57709d and 3111e67ecae517aa86590e8cb6c58c549e090fff.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `packages/ai-native/src/browser/components/components.module.less` (1 hunks)
* `packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* packages/ai-native/src/browser/components/components.module.less

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (8)</summary>

* GitHub Check: unittest (ubuntu-latest, 18.x, jsdom)
* GitHub Check: build (ubuntu-latest, 20.x)
* GitHub Check: unittest (ubuntu-latest, 18.x, node)
* GitHub Check: build (macos-latest, 20.x)
* GitHub Check: unittest (macos-latest, 18.x, jsdom)
* GitHub Check: unittest (macos-latest, 18.x, node)
* GitHub Check: build-windows
* GitHub Check: ubuntu-latest, Node.js 20.x

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts (1)</summary>

`16-16`: **接口导入正确**

正确导入了`IImageUploadProviderRegistry`接口，用于支持图片上传功能。

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T08:21:23Z
user: login: ensorrow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T08:59:24Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>packages/ai-native/src/browser/chat/chat-model.ts (1)</summary><blockquote>

`333-341`: **实现了聊天历史中的图片内容支持**

代码现在可以将图片作为聊天内容的一部分添加到消息历史中，实现方式合理。不过有两个需要注意的地方：

1. 代码假设 `request.message.images` 在检查 `.length` 时始终已定义。如果 `images` 是 undefined，这可能会导致运行时错误。
2. 直接使用 `new URL(image)` 创建URL对象可能存在安全风险，如果输入的URL格式不正确可能会抛出异常。


建议修改代码以更安全地处理这些情况：

```diff
      history.push({
        role: 'user',
-        content: request.message.images?.length
+        content: request.message.images && request.message.images.length > 0
          ? [
              { type: 'text', text: request.message.prompt },
-              ...request.message.images.map((image) => ({ type: 'image', image: new URL(image) } as ImagePart)),
+              ...request.message.images.map((image) => {
+                try {
+                  return { type: 'image', image: new URL(image) } as ImagePart
+                } catch (e) {
+                  console.warn('Invalid image URL:', image);
+                  return null;
+                }
+              }).filter(Boolean),
            ]
          : request.message.prompt,
      });
```

</blockquote></details>
<details>
<summary>packages/ai-native/src/browser/components/ChatMentionInput.tsx (2)</summary><blockquote>

`257-274`: **实现了图片上传按钮**

上传图片的按钮实现得当，但有几点可以优化：
1. 每次点击都创建新的input元素不太高效
2. 缺少上传过程中的加载状态指示


建议使用React的ref存储input元素并重用，同时添加上传状态指示：

```diff
+const fileInputRef = React.useRef<HTMLInputElement>(null);
+const [isUploading, setIsUploading] = useState(false);

+useEffect(() => {
+  const input = document.createElement('input');
+  input.type = 'file';
+  input.accept = 'image/*';
+  input.onchange = (e) => {
+    const file = (e.target as HTMLInputElement).files?.[0];
+    if (file) {
+      handleImageUpload(file);
+    }
+  };
+  fileInputRef.current = input;
+}, [handleImageUpload]);

// 在按钮配置中
{
  id: 'upload-image',
  iconClass: 'codicon codicon-file-media',
  title: 'Upload Image',
-  onClick: () => {
-    const input = document.createElement('input');
-    input.type = 'file';
-    input.accept = 'image/*';
-    input.onchange = (e) => {
-      const file = (e.target as HTMLInputElement).files?.[0];
-      if (file) {
-        handleImageUpload(file);
-      }
-    };
-    input.click();
-  },
+  onClick: () => fileInputRef.current?.click(),
+  disabled: isUploading,
  position: FooterButtonPosition.LEFT,
}
```

---

`346-365`: **图片预览组件需要增强可访问性**

`ImagePreviewer`组件实现了基本功能，但可访问性方面有待改进。


建议增强删除按钮的可访问性属性：

```diff
<button 
  onClick={() => onDelete(index)} 
  className={styles.delete_button}
+ aria-label="删除图片"
+ title="删除图片"
>
  <Icon iconClass='codicon codicon-close' />
</button>
```

另外，建议为图片添加alt文本以提高可访问性：

```diff
-<Image src={image.toString()} />
+<Image 
+  src={image.toString()} 
+  alt={`上传的图片 ${index + 1}`}
+  onError={() => messageService.error(`无法加载图片 ${index + 1}`)}
+/>
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 3111e67ecae517aa86590e8cb6c58c549e090fff and 5ea82e34b873c564cf839b004588038208e3ce44.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `packages/ai-native/src/browser/chat/chat-model.ts` (2 hunks)
* `packages/ai-native/src/browser/components/ChatMentionInput.tsx` (6 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: build (ubuntu-latest, 20.x)
* GitHub Check: build-windows
* GitHub Check: build (macos-latest, 20.x)
* GitHub Check: ubuntu-latest, Node.js 20.x

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>packages/ai-native/src/browser/chat/chat-model.ts (1)</summary>

`30-30`: **导入了必要的 ImagePart 类型**

新增导入的 ImagePart 类型用于支持聊天消息中的图片内容，这是实现图片上传功能的基础。

</details>
<details>
<summary>packages/ai-native/src/browser/components/ChatMentionInput.tsx (5)</summary>

`1-1`: **添加了处理图片上传所需的导入**

这些新的导入很好地支持了图片上传和预览功能的实现，包括：
- 从'ai'导入`DataContent`类型
- 图片组件的导入
- 消息服务的导入
- 图片上传提供者注册相关的导入




Also applies to: 4-4, 11-11, 16-16, 18-18

---

`24-31`: **正确地更新了接口以支持图片上传功能**

`IChatMentionInputProps`接口得到了良好的扩展，增加了可选的`images`参数到`onSend`方法签名中，以及新增了`images`属性，使组件能够接收和处理图片数据。




Also applies to: 40-40

---

`56-56`: **新增了图片状态和服务注入**

组件正确地添加了图片状态管理，并注入了必要的消息服务和图片上传提供者注册服务。




Also applies to: 64-65

---

`290-297`: **正确更新了消息发送处理**

`handleSend`方法现在包含图片数据，并在发送后重置图片状态，符合预期行为。

---

`330-330`: **添加了图片预览组件的条件渲染**

良好地使用条件渲染，只在有图片时显示预览组件。

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T09:01:41Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>packages/ai-native/src/browser/components/ChatMentionInput.tsx (1)</summary><blockquote>

`300-317`: _🛠️ Refactor suggestion_

**图片上传处理函数缺少文件大小限制**

图片上传处理函数中检查了文件类型，但没有检查文件大小限制，可能导致上传过大的图片。虽然在 `imageToBase64` 函数中有大小限制，但在上传前就应该进行检查，以提供更好的用户体验。


建议添加文件大小限制和上传状态管理：

```diff
const handleImageUpload = useCallback(
  async (file: File) => {
    const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
    if (!allowedTypes.includes(file.type)) {
      messageService.error('Only JPG, PNG, WebP and GIF images are supported');
      return;
    }

+   const MAX_FILE_SIZE = 3 * 1024 * 1024; // 3MB，与 ai-native.contribution.ts 中保持一致
+   if (file.size > MAX_FILE_SIZE) {
+     messageService.error('图片大小超过 3MB 限制');
+     return;
+   }

    const imageUploadProvider = chatFeatureRegistry.getImageUploadProvider();
    if (!imageUploadProvider) {
      messageService.error('No image upload provider found');
      return;
    }
+   try {
+     // 可以添加一个状态来显示加载中
+     // setIsUploading(true);
      const data = await imageUploadProvider.imageUpload(file);
      setImages([...images, data]);
+   } catch (error) {
+     messageService.error(`上传图片失败: ${error.message}`);
+   } finally {
+     // setIsUploading(false);
+   }
  },
  [images],
);
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>packages/ai-native/src/browser/components/ChatMentionInput.tsx (1)</summary><blockquote>

`344-363`: **实现了图片预览组件**

添加了一个新组件来显示上传的图片，并提供删除功能。实现干净简洁，遵循 React 模式。建议考虑添加一些辅助功能，如图片加载状态、失败回退和可访问性支持。

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5ea82e34b873c564cf839b004588038208e3ce44 and 07da4e909fa641ad7d7efeb3a0000e53840488cb.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `packages/ai-native/src/browser/chat/chat.feature.registry.ts` (2 hunks)
* `packages/ai-native/src/browser/components/ChatMentionInput.tsx` (7 hunks)
* `packages/ai-native/src/browser/types.ts` (4 hunks)
* `packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts` (2 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: build-windows
* GitHub Check: unittest (macos-latest, 18.x, jsdom)
* GitHub Check: build (macos-latest, 20.x)
* GitHub Check: ubuntu-latest, Node.js 20.x

</details>

<details>
<summary>🔇 Additional comments (17)</summary><blockquote>

<details>
<summary>packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts (3)</summary>

`235-237`: **添加了图片上传提供者注册功能**

该变更在 `ChatFeatureRegistry` 上添加了图片上传功能支持，并使用了 `imageToBase64` 函数作为上传处理器，实现了基本的图片转换与上传功能。

---

`513-514`: **添加了合理的图片大小限制**

设置了 3MB 的图片大小限制常量，这有助于防止过大图片导致的性能问题和存储压力。

---

`515-531`: **良好的图片转换实现与错误处理**

`imageToBase64` 函数实现了完整的图片转换流程：
1. 检查文件大小是否超过限制
2. 使用 FileReader 将文件转换为 base64 字符串
3. 适当处理错误情况

代码实现了完善的错误处理，并在转换过程中提供了明确的错误信息。

</details>
<details>
<summary>packages/ai-native/src/browser/chat/chat.feature.registry.ts (3)</summary>

`5-5`: **导入了必要的图片上传提供者接口**

添加了 `IImageUploadProvider` 到导入列表中，用于支持图片上传功能的类型定义。

---

`15-15`: **添加了图片上传提供者的私有成员变量**

添加了 `imageUploadProvider` 私有成员变量用于存储注册的图片上传提供者实例。

---

`17-23`: **实现了图片上传提供者的注册与获取方法**

添加了两个公共方法：
- `registerImageUploadProvider`: 注册图片上传提供者
- `getImageUploadProvider`: 获取注册的图片上传提供者

这两个方法实现简洁明了，与类中其他注册方法保持一致的模式，符合代码库的设计风格。

</details>
<details>
<summary>packages/ai-native/src/browser/types.ts (5)</summary>

`1-1`: **添加必要的数据类型导入**

从 'ai' 包导入了 `DataContent` 类型，用于支持图片上传功能的类型定义。

---

`133-133`: **扩展了聊天功能注册接口**

在 `IChatFeatureRegistry` 接口中添加了 `registerImageUploadProvider` 方法，使图片上传提供者可以被注册到聊天功能中。这与 `ChatFeatureRegistry` 类中的实现保持一致。

---

`145-145`: **扩展了用户角色渲染类型**

在 `ChatUserRoleRender` 类型中添加了可选的 `images` 属性，使渲染函数能够显示图片。

---

`152-152`: **更新了聊天输入渲染类型的发送方法签名**

更新了 `ChatInputRender` 类型的 `onSend` 方法签名，添加了可选的 `images` 参数，使输入组件能够发送图片。

---

`295-297`: **定义了图片上传提供者接口**

添加了 `IImageUploadProvider` 接口，定义了 `imageUpload` 方法，该方法接受一个 `File` 并返回一个 `Promise<DataContent | URL>`。接口定义清晰明确，为图片上传功能提供了良好的类型支持。

</details>
<details>
<summary>packages/ai-native/src/browser/components/ChatMentionInput.tsx (6)</summary>

`24-30`: **更新了聊天输入属性接口**

在 `IChatMentionInputProps` 接口中更新了 `onSend` 方法签名，添加了 `images` 参数，并添加了 `images` 属性，使组件能够接收和发送图片。这与 `types.ts` 文件中的变更保持一致。

---

`55-55`: **添加了图片状态管理**

在组件中添加了图片状态管理，从 props 初始化。这是管理组件状态的良好实践。

---

`255-272`: **添加了图片上传按钮功能**

添加了用于上传图片的按钮和相关功能，包括：
1. 创建文件输入元素
2. 设置接受的文件类型为图片
3. 处理文件选择事件
4. 调用图片上传处理函数

这是一个常见且有效的文件上传模式。

---

`288-296`: **更新了发送方法以包含图片**

更新了 `handleSend` 方法，以便在发送消息时包含图片。实现还在发送后清除图片状态，这是防止重复发送相同图片的良好做法。

---

`319-324`: **添加了图片删除功能**

添加了从预览中删除图片的方法。实现简洁明了，按预期工作。

---

`328-328`: **添加了图片预览组件**

添加了 `ImagePreviewer` 组件来显示上传的图片。这是一个好的添加，为用户提供了视觉反馈。

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T09:36:24Z
user: login: ensorrow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T02:07:14Z
user: login: erha19
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T02:15:07Z
user: login: ensorrow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T02:27:48Z
user: login: ensorrow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T02:28:24Z
user: login: ensorrow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T02:31:45Z
user: login: erha19
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-17T02:56:26Z

Review comments:
diff_hunk: @@ -508,4 +509,11 @@ export class AINativeContribution implements AINativeCoreContribution {
       useClass: DefaultChatAgentPromptProvider,
     });
   }
+
+  registerImageUploadProvider(registry: IImageUploadProviderRegistry): void {
+    registry.registerImageUploadProvider({
+      imageUpload: async (file) =>
+        'https://img.alicdn.com/imgextra/i2/O1CN01dqjQei1tpbj9z9VPH_!!6000000005951-55-tps-87-78.svg',
+    });
+  }
path: packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**图片上传提供者实现需要改进**

当前实现将任何上传的图片都映射到一个硬编码的图片 URL，适合样例展示但不适合生产环境。

建议：
1. 实现真实的文件上传逻辑，处理不同的文件类型和大小
2. 添加错误处理机制
3. 考虑添加进度反馈

```diff
registerImageUploadProvider(registry: IImageUploadProviderRegistry): void {
  registry.registerImageUploadProvider({
-    imageUpload: async (file) =>
-      'https://img.alicdn.com/imgextra/i2/O1CN01dqjQei1tpbj9z9VPH_!!6000000005951-55-tps-87-78.svg',
+    imageUpload: async (file) => {
+      try {
+        // 实际上传逻辑，例如：
+        // const formData = new FormData();
+        // formData.append('file', file);
+        // const response = await fetch('/api/upload', { method: 'POST', body: formData });
+        // const data = await response.json();
+        // return data.url;
+        
+        // 开发环境临时实现：
+        await new Promise(resolve => setTimeout(resolve, 500)); // 模拟网络延迟
+        return 'https://img.alicdn.com/imgextra/i2/O1CN01dqjQei1tpbj9z9VPH_!!6000000005951-55-tps-87-78.svg';
+      } catch (error) {
+        console.error('图片上传失败', error);
+        throw new Error('图片上传失败');
+      }
+    },
  });
}
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  registerImageUploadProvider(registry: IImageUploadProviderRegistry): void {
    registry.registerImageUploadProvider({
      imageUpload: async (file) => {
        try {
          // 实际上传逻辑，例如：
          // const formData = new FormData();
          // formData.append('file', file);
          // const response = await fetch('/api/upload', { method: 'POST', body: formData });
          // const data = await response.json();
          // return data.url;
          
          // 开发环境临时实现：
          await new Promise(resolve => setTimeout(resolve, 500)); // 模拟网络延迟
          return 'https://img.alicdn.com/imgextra/i2/O1CN01dqjQei1tpbj9z9VPH_!!6000000005951-55-tps-87-78.svg';
        } catch (error) {
          console.error('图片上传失败', error);
          throw new Error('图片上传失败');
        }
      },
    });
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 3111e67
created_at: 2025-03-13T14:10:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -139,6 +153,12 @@ export class FileSearchService implements IFileSearchService {
     return new Promise((resolve, reject) => {
       try {
         const args = this.getSearchArgs(options);
+
+        if (options.onlyFolders) {
+          args.push('--type-list', 'd:dir');
+          args.push('--type', 'd');
+        }
path: packages/file-search/src/node/file-search.service.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**代码重复：相同的文件夹过滤参数在两处添加**

在`doFind`和`getSearchArgs`方法中都添加了相同的文件夹过滤参数。由于`doFind`方法已经调用了`getSearchArgs`获取参数，因此在`doFind`中再次添加这些参数会导致重复。

建议删除`doFind`方法中的重复代码（157-160行），仅保留`getSearchArgs`中的实现。


```diff
- if (options.onlyFolders) {
-   args.push('--type-list', 'd:dir');
-   args.push('--type', 'd');
- }
```


Also applies to: 186-189

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-13T14:10:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,333 @@
+.input_container {
+  position: relative;
+  width: 100%;
+  margin: 0 auto;
+  border-radius: 4px;
+
+  .model_selector {
+    margin-right: 5px;
+  }
+
+  .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;
+    text-wrap: nowrap;
+    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);
+}
path: packages/ai-native/src/browser/components/mention-input/mention-input.module.less
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**提及面板位置固定在输入框上方时，应留意在小屏幕或折叠面板场景的可见性**  
`.mention_panel_container` 使用了 `transform: translateY(-100%)`，如在极小屏幕或外层容器高度不足的情况下，需要确保面板不会超出视窗。

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-13T14:10:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,969 @@
+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 = '&nbsp;';
+
+export const MentionInput: React.FC<MentionInputProps> = ({
+  mentionItems = [],
+  onSend,
+  onStop,
+  loading = false,
+  mentionKeyword = MENTION_KEYWORD,
+  onSelectionChange,
+  onImageUpload,
+  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 handlePaste = (e: React.ClipboardEvent<HTMLDivElement>) => {
+    e.preventDefault();
+    const items = e.clipboardData.items;
+    // eslint-disable-next-line @typescript-eslint/prefer-for-of
+    for (let i = 0; i < items.length; i++) {
+      if (items[i].kind === 'file' && items[i].type.startsWith('image/')) {
+        const file = items[i].getAsFile();
+        if (file && onImageUpload) {
+          onImageUpload(file);
+        }
+      }
+    }
+  };
+
+  // 处理键盘事件
+  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();
+  };
path: packages/ai-native/src/browser/components/mention-input/mention-input.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**在编辑器中插入 DOM 元素时需注意可能的 XSS 风险**  
虽然此处使用 `document.createTextNode()` 替换了文本节点，但整体仍依赖 `editorRef.current.innerHTML` 进行处理。若任何不受信任的外部输入或字符串未正确过滤，即便在本地应用场景下也可能潜藏风险，建议在处理粘贴内容或外部文本时增加 HTML Sanitization。

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-13T14:10:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,969 @@
+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 = '&nbsp;';
+
+export const MentionInput: React.FC<MentionInputProps> = ({
+  mentionItems = [],
+  onSend,
+  onStop,
+  loading = false,
+  mentionKeyword = MENTION_KEYWORD,
+  onSelectionChange,
+  onImageUpload,
+  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 handlePaste = (e: React.ClipboardEvent<HTMLDivElement>) => {
+    e.preventDefault();
+    const items = e.clipboardData.items;
+    // eslint-disable-next-line @typescript-eslint/prefer-for-of
+    for (let i = 0; i < items.length; i++) {
+      if (items[i].kind === 'file' && items[i].type.startsWith('image/')) {
+        const file = items[i].getAsFile();
+        if (file && onImageUpload) {
+          onImageUpload(file);
+        }
+      }
+    }
+  };
+
+  // 处理键盘事件
+  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}
+          onPaste={handlePaste}
+          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>
path: packages/ai-native/src/browser/components/mention-input/mention-input.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**提及标签处理与模型选择功能集成度高，建议加强单元测试覆盖**  
当前 `handleSend` 等流程将最终生成替换后的内容再提交，有一定字符串操作复杂度；添加针对各种 mention、粘贴场景和多选项模型的测试用例可降低回归风险。  


如果需要，我可以提供单元测试示例来帮助验证各种边界场景，并协助完善测试覆盖率。

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-13T14:10:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -634,15 +672,50 @@
         msgId,
       });
     },
-    [chatRenderRegistry, chatRenderRegistry.chatUserRoleRender, msgHistoryManager, scrollToBottom],
+    [chatRenderRegistry, chatRenderRegistry.chatUserRoleRender, msgHistoryManager, scrollToBottom, loading],
   );
 
   const handleSend = React.useCallback(
-    async (value: IChatMessageStructure) => {
-      const { message, command, reportExtra } = value;
+    async (message: string, images?: string[], agentId?: string, command?: string) => {
+      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;
-      return handleAgentReply({ message, agentId, command, reportExtra });
+      const folderPattern = /\{\{@folder:(.*?)\}\}/g;
+      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, images, agentId, command, reportExtra });
path: packages/ai-native/src/browser/chat/chat.view.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**在 `handleSend` 中对文件/文件夹占位符进行处理逻辑恰当，但需注意性能。**  
在复杂场景下，正则匹配和多次 `addFileToContext`/`addFolderToContext` 可能改变性能表现，后续若规模扩大可考虑批量处理。

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-13T14:10:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -790,17 +863,7 @@
               />
             )}
             <ChatInputWrapperRender
-              onSend={(value, agentId, command) =>
-                handleSend({
-                  message: value,
-                  agentId,
-                  command,
-                  reportExtra: {
-                    actionSource: ActionSourceEnum.Chat,
-                    actionType: ActionTypeEnum.Send,
-                  },
-                })
-              }
+              onSend={handleSend}
path: packages/ai-native/src/browser/chat/chat.view.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**类型不匹配导致管线报错，需尽快修复。**  
当前的 `onSend` 类型定义为 `(...args) => void`，但实际实现为异步 `Promise<void>`。请在 `ChatInputRender` 或此处对类型进行统一修改，避免后续构建失败。  

```diff
- onSend: (value: string, images?: string[], agentId?: string, command?: string) => void;
+ onSend: (value: string, images?: string[], agentId?: string, command?: string) => Promise<void>;
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
// File: packages/ai-native/src/browser/chat/chat.view.tsx

// Assuming the original interface looked like this:
interface ChatInputRenderProps {
  // Other props...
-  onSend: (value: string, images?: string[], agentId?: string, command?: string) => void;
+  onSend: (value: string, images?: string[], agentId?: string, command?: string) => Promise<void>;
  // Other props...
}

export const ChatInputRender: React.FC<ChatInputRenderProps> = ({ onSend, ...props }) => {
  // Component implementation...
  return (
    <div>
      {/* Other component code */}
      {/* Usage remains unchanged */}
      <button onClick={() => onSend("sample message")}>Send</button>
    </div>
  );
};

// And elsewhere in the file, where the component is used:
export const ChatView = () => {
  const handleSend = async (
    value: string,
    images?: string[],
    agentId?: string,
    command?: string
  ): Promise<void> => {
    // Async implementation...
  };

  return (
    <ChatInputRender
      onSend={handleSend}
      // Other props...
    />
  );
};
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: unittest (ubuntu-latest, 18.x, jsdom)</summary>

[failure] 866-866: 
Type '(message: string, images?: string[], agentId?: string, command?: string) => Promise<void>' is not assignable to type '((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined, op...'.

</details>
<details>
<summary>🪛 GitHub Check: unittest (ubuntu-latest, 18.x, node)</summary>

[failure] 866-866: 
Type '(message: string, images?: string[], agentId?: string, command?: string) => Promise<void>' is not assignable to type '((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined, op...'.

</details>
<details>
<summary>🪛 GitHub Check: build (ubuntu-latest, 20.x)</summary>

[failure] 866-866: 
Type '(message: string, images?: string[], agentId?: string, command?: string) => Promise<void>' is not assignable to type '((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined, op...'.

</details>
<details>
<summary>🪛 GitHub Check: unittest (macos-latest, 18.x, jsdom)</summary>

[failure] 866-866: 
Type '(message: string, images?: string[], agentId?: string, command?: string) => Promise<void>' is not assignable to type '((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined, op...'.

</details>
<details>
<summary>🪛 GitHub Check: ubuntu-latest, Node.js 20.x</summary>

[failure] 866-866: 
Type '(message: string, images?: string[], agentId?: string, command?: string) => Promise<void>' is not assignable to type '((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined, op...'.

</details>
<details>
<summary>🪛 GitHub Actions: E2E Test</summary>

[error] 866-866: Type '(message: string, images?: string[], agentId?: string, command?: string) => Promise<void>' is not assignable to type '((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined, op...'.

</details>
<details>
<summary>🪛 GitHub Actions: CI</summary>

[error] 866-866: error TS2322: Type '(message: string, images?: string[], agentId?: string, command?: string) => Promise<void>' is not assignable to type '((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, agentId?: string | undefined, command?: string | undefined) => void) & ((value: string, images?: string[] | undefined, agentId?: string | undefined, command?: string | undefined, op...'.

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-13T14:10:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -140,13 +141,14 @@ export type ChatWelcomeRender = (props: {
 export type ChatAIRoleRender = (props: { content: string }) => React.ReactElement | React.JSX.Element;
 export type ChatUserRoleRender = (props: {
   content: string;
+  images?: string[];
   agentId?: string;
   command?: string;
 }) => React.ReactElement | React.JSX.Element;
 export type ChatThinkingRender = (props: { thinkingText?: string }) => React.ReactElement | React.JSX.Element;
 export type ChatThinkingResultRender = (props: { thinkingResult?: string }) => React.ReactElement | React.JSX.Element;
 export type ChatInputRender = (props: {
-  onSend: (value: string, agentId?: string, command?: string) => void;
+  onSend: (value: string, images?: string[], agentId?: string, command?: string) => void;
path: packages/ai-native/src/browser/types.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**`ChatInputRender` 的 `onSend` 方法签名也扩展了 `images`。**  
保持了前后端一致性，但请确保与实际实现对齐并解决返回类型不符的问题。

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-13T14:10:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -508,4 +509,11 @@ export class AINativeContribution implements AINativeCoreContribution {
       useClass: DefaultChatAgentPromptProvider,
     });
   }
+
+  registerImageUploadProvider(registry: IImageUploadProviderRegistry): void {
path: packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts
user: login: Ricbet
body: 感觉可以注册在 IChatFeatureRegistry 里，语义更明确一些。就能知道这个 image upload 是服务于 chat 面板的
created_at: 2025-03-14T01:41:56Z
author_association: MEMBER
diff_hunk: @@ -508,4 +509,11 @@ export class AINativeContribution implements AINativeCoreContribution {
       useClass: DefaultChatAgentPromptProvider,
     });
   }
+
+  registerImageUploadProvider(registry: IImageUploadProviderRegistry): void {
+    registry.registerImageUploadProvider({
+      imageUpload: async (file) =>
path: packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts
user: login: erha19
body: 这个删了吧。
created_at: 2025-03-14T04:02:53Z
author_association: MEMBER
diff_hunk: @@ -50,6 +50,7 @@
     display: flex;
     align-items: center;
     cursor: pointer;
+    text-wrap: nowrap;
path: packages/ai-native/src/browser/components/mention-input/mention-input.module.less
user: login: erha19
body: 这个属性要删了
created_at: 2025-03-14T05:59:37Z
author_association: MEMBER
diff_hunk: @@ -205,3 +214,13 @@
 // Checkbox
 @checkbox-default-size: 12px;
 @checkbox-large-size: 14px;
+
+// Image
+@image-size-base: 48px;
+@image-font-size-base: 24px;
+@image-bg: #f5f5f5;
+@image-color: #fff;
path: packages/components/src/style/variable.less
user: login: erha19
body: 这里的颜色没考虑主题色，可以参考 hisotry 面板用一下背景色
created_at: 2025-03-14T06:01:08Z
author_association: MEMBER
diff_hunk: @@ -205,3 +214,13 @@
 // Checkbox
 @checkbox-default-size: 12px;
 @checkbox-large-size: 14px;
+
+// Image
+@image-size-base: 48px;
+@image-font-size-base: 24px;
+@image-bg: #f5f5f5;
+@image-color: #fff;
+@image-mask-font-size: 16px;
+@image-preview-operation-size: 18px;
+@image-preview-operation-color: @text-color-dark;
+@image-preview-operation-disabled-color: fade(@image-preview-operation-color, 25%);
path: packages/components/src/style/variable.less
user: login: erha19
body: 这里也是，尽量使用 token 颜色替换吧
created_at: 2025-03-14T06:02:00Z
author_association: MEMBER
diff_hunk: @@ -508,4 +509,11 @@ export class AINativeContribution implements AINativeCoreContribution {
       useClass: DefaultChatAgentPromptProvider,
     });
   }
+
+  registerImageUploadProvider(registry: IImageUploadProviderRegistry): void {
path: packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts
user: login: ensorrow
body: 很有道理，我改下
created_at: 2025-03-14T08:59:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -254,13 +287,47 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
       if (disabled) {
         return;
       }
-      onSend(content, undefined, undefined, option);
+      onSend(
+        content,
+        images.map((image) => image.toString()),
+        undefined,
+        undefined,
+        option,
+      );
+      setImages(props.images || []);
     },
-    [onSend, editorService, disabled],
+    [onSend, images, disabled],
+  );
+
+  const handleImageUpload = useCallback(
+    async (file: File) => {
+      const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
+      if (!allowedTypes.includes(file.type)) {
+        messageService.error('Only JPG, PNG, WebP and GIF images are supported');
+        return;
+      }
+
+      const imageUploadProvider = imageUploadProviderRegistry.getImageUploadProvider();
+      if (!imageUploadProvider) {
+        messageService.error('No image upload provider found');
+        return;
+      }
+      const data = await imageUploadProvider.imageUpload(file);
+      setImages([...images, data]);
+    },
+    [images],
+  );
path: packages/ai-native/src/browser/components/ChatMentionInput.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**图片上传处理缺少文件大小限制**

`handleImageUpload`方法检查了文件类型，但没有检查文件大小限制，可能导致上传过大的图片。


建议添加文件大小限制和上传状态管理：

```diff
const handleImageUpload = useCallback(
  async (file: File) => {
    const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
    if (!allowedTypes.includes(file.type)) {
      messageService.error('Only JPG, PNG, WebP and GIF images are supported');
      return;
    }

+   const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
+   if (file.size > MAX_FILE_SIZE) {
+     messageService.error('File size exceeds 5MB limit');
+     return;
+   }

    const imageUploadProvider = imageUploadProviderRegistry.getImageUploadProvider();
    if (!imageUploadProvider) {
      messageService.error('No image upload provider found');
      return;
    }
+   try {
+     setIsUploading(true);
      const data = await imageUploadProvider.imageUpload(file);
      setImages([...images, data]);
+   } catch (error) {
+     messageService.error(`Failed to upload image: ${error.message}`);
+   } finally {
+     setIsUploading(false);
+   }
  },
  [images],
);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  const handleImageUpload = useCallback(
    async (file: File) => {
      const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
      if (!allowedTypes.includes(file.type)) {
        messageService.error('Only JPG, PNG, WebP and GIF images are supported');
        return;
      }

      const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
      if (file.size > MAX_FILE_SIZE) {
        messageService.error('File size exceeds 5MB limit');
        return;
      }

      const imageUploadProvider = imageUploadProviderRegistry.getImageUploadProvider();
      if (!imageUploadProvider) {
        messageService.error('No image upload provider found');
        return;
      }
      try {
        setIsUploading(true);
        const data = await imageUploadProvider.imageUpload(file);
        setImages([...images, data]);
      } catch (error) {
        messageService.error(`Failed to upload image: ${error.message}`);
      } finally {
        setIsUploading(false);
      }
    },
    [images],
  );
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 07da4e9
created_at: 2025-03-14T09:01:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,6 +50,7 @@
     display: flex;
     align-items: center;
     cursor: pointer;
+    text-wrap: nowrap;
path: packages/ai-native/src/browser/components/mention-input/mention-input.module.less
user: login: ensorrow
body: <img width="774" alt="image" src="https://github.com/user-attachments/assets/8196347d-0d67-4ebc-adca-69d341a76b30" />
看另一个地方也有，是有问题的吗
created_at: 2025-03-17T02:07:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,6 +50,7 @@
     display: flex;
     align-items: center;
     cursor: pointer;
+    text-wrap: nowrap;
path: packages/ai-native/src/browser/components/mention-input/mention-input.module.less
user: login: erha19
body: 可以删了，这个样式一方面没啥用，还会在 console 上带个 warning
created_at: 2025-03-17T02:15:07Z
author_association: MEMBER
diff_hunk: @@ -205,3 +214,13 @@
 // Checkbox
 @checkbox-default-size: 12px;
 @checkbox-large-size: 14px;
+
+// Image
+@image-size-base: 48px;
+@image-font-size-base: 24px;
+@image-bg: #f5f5f5;
+@image-color: #fff;
path: packages/components/src/style/variable.less
user: login: ensorrow
body: 这里是 placeholder 用的，目前还没用到 image placeholder 的能力
created_at: 2025-03-17T02:27:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -205,3 +214,13 @@
 // Checkbox
 @checkbox-default-size: 12px;
 @checkbox-large-size: 14px;
+
+// Image
+@image-size-base: 48px;
+@image-font-size-base: 24px;
+@image-bg: #f5f5f5;
+@image-color: #fff;
+@image-mask-font-size: 16px;
+@image-preview-operation-size: 18px;
+@image-preview-operation-color: @text-color-dark;
+@image-preview-operation-disabled-color: fade(@image-preview-operation-color, 25%);
path: packages/components/src/style/variable.less
user: login: ensorrow
body: 这个暂时也没用
created_at: 2025-03-17T02:28:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,6 +50,7 @@
     display: flex;
     align-items: center;
     cursor: pointer;
+    text-wrap: nowrap;
path: packages/ai-native/src/browser/components/mention-input/mention-input.module.less
user: login: ensorrow
body: OK
created_at: 2025-03-17T02:31:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -239,6 +252,24 @@ export const ChatMentionInput = (props: IChatMentionInputProps) => {
           onClick: handleShowMCPConfig,
           position: FooterButtonPosition.LEFT,
         },
+        {
+          id: 'upload-image',
+          iconClass: 'codicon codicon-file-media',
+          title: 'Upload Image',
path: packages/ai-native/src/browser/components/ChatMentionInput.tsx
user: login: erha19
body: i18n
created_at: 2025-03-17T02:55:28Z
author_association: MEMBER

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-13T14:10:09Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
本次变更在多个模块中增加了对图像数据的支持。主要更新了聊天服务、消息历史、UI组件、类型定义以及样式文件，统一在各处方法和接口中添加了可选的 `images` 参数或属性。同时，新增加了图片上传逻辑、图片预览组件以及图片显示组件，并在上下文和模型处理流程中融入了图像处理和文件转换功能，确保链路中各部分均能处理图像数据。

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `packages/ai-native/src/browser/chat/…/chat-manager.service.ts`, `chat-proxy.service.ts`, `chat.internal.service.ts`, `chat.view.tsx`, `chat-model.ts`, `msg-history-manager.ts`, `common/index.ts`, `node/base-language-model.ts`, `core-common/src/types/ai-native/index.ts`, `browser/types.ts` | 更新方法签名和类型定义，在聊天服务、消息模型、请求和历史记录中添加 `images?: string[]` 参数，以支持图像数据的传递与处理。 |
| `packages/ai-native/src/browser/components/ChatMentionInput.tsx`, `mention-input/mention-input.tsx`, `mention-input/types.ts`, `ChatEditor.tsx`, `ChatInput.tsx` | 加强聊天输入组件，新增图片上传处理、图片预览器以及更新相关接口和事件处理，实现文本与图像的同时发送。 |
| `packages/ai-native/src/browser/context/llm-context.service.ts`, `common/llm-context.ts` | 引入自动采集、文件夹上下文管理及异步序列化方法，扩展上下文管理的功能。 |
| `packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts`, `packages/ai-native/src/browser/chat/chat.feature.registry.ts` | 新增注册图片上传服务的功能，包含文件大小校验、文件转换为 Base64 的逻辑，并提供图片上传服务的注册与获取方法。 |
| `packages/components/package.json`, `src/image/index.tsx`, `src/image/styles.less`, `src/style/mixins.less`, `src/style/variable.less`, `src/index.ts` | 添加新的图片组件模块（基于 `rc-image`），并增加相应样式、LESS 变量和混入方法，支持图片展示与预览功能。 |
| `packages/core-browser/src/components/ai-native/enhanceIcon/styles.module.less` | 移除冗余的 CSS 属性，调整图标样式。 |

## Sequence Diagram(s)
```mermaid
sequenceDiagram
    participant User as 用户
    participant UI as 聊天UI组件
    participant ImgProv as 图片上传服务
    participant ChatMgr as ChatManagerService
    participant ChatProxy as ChatProxyService
    participant LangModel as BaseLanguageModel

    User->>UI: 上传/粘贴图片
    UI->>ImgProv: 调用 onImageUpload(file)
    ImgProv-->>UI: 返回图片 URL
    User->>UI: 发送带图片的消息
    UI->>ChatMgr: 调用 createRequest(message, images, …)
    ChatMgr->>ChatProxy: 调用 sendRequest(… images …)
    ChatProxy->>LangModel: 调用 request(message, images, …)
```

## Suggested reviewers
- life2015
- hacke2

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡🧪 Multi-step agentic review comment chat (experimental)</summary>
> 
> - We're introducing multi-step agentic chat in review comments. This experimental feature enhances review discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments.
> 						- To enable this feature, set early_access to true under in the settings.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 07da4e909fa641ad7d7efeb3a0000e53840488cb and 08b10be5cd79cb416a4ef78cb42400d3ea87edf7.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `packages/core-browser/src/components/ai-native/enhanceIcon/styles.module.less` (0 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* packages/core-browser/src/components/ai-native/enhanceIcon/styles.module.less

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (8)</summary>

* GitHub Check: ubuntu-latest, Node.js 20.x
* GitHub Check: build (ubuntu-latest, 20.x)
* GitHub Check: build-windows
* GitHub Check: build (macos-latest, 20.x)
* GitHub Check: unittest (ubuntu-latest, 18.x, node)
* GitHub Check: unittest (ubuntu-latest, 18.x, jsdom)
* GitHub Check: unittest (macos-latest, 18.x, jsdom)
* GitHub Check: unittest (macos-latest, 18.x, node)

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=opensumi/core&utm_content=4476):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-03-14T04:18:56Z
body: ## [Codecov](https://app.codecov.io/gh/opensumi/core/pull/4476?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi) Report
Attention: Patch coverage is `4.54545%` with `21 lines` in your changes missing coverage. Please review.
> Project coverage is 53.15%. Comparing base [(`82ddf9a`)](https://app.codecov.io/gh/opensumi/core/commit/82ddf9af7acbcc85ea154022753c8c060b6f84ce?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi) to head [(`08b10be`)](https://app.codecov.io/gh/opensumi/core/commit/08b10be5cd79cb416a4ef78cb42400d3ea87edf7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi).
> Report is 4 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/opensumi/core/pull/4476?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi) | Patch % | Lines |
|---|---|---|
| [packages/ai-native/src/node/base-language-model.ts](https://app.codecov.io/gh/opensumi/core/pull/4476?src=pr&el=tree&filepath=packages%2Fai-native%2Fsrc%2Fnode%2Fbase-language-model.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi#diff-cGFja2FnZXMvYWktbmF0aXZlL3NyYy9ub2RlL2Jhc2UtbGFuZ3VhZ2UtbW9kZWwudHM=) | 0.00% | [7 Missing :warning: ](https://app.codecov.io/gh/opensumi/core/pull/4476?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi) |
| [packages/ai-native/src/browser/chat/chat-model.ts](https://app.codecov.io/gh/opensumi/core/pull/4476?src=pr&el=tree&filepath=packages%2Fai-native%2Fsrc%2Fbrowser%2Fchat%2Fchat-model.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi#diff-cGFja2FnZXMvYWktbmF0aXZlL3NyYy9icm93c2VyL2NoYXQvY2hhdC1tb2RlbC50cw==) | 0.00% | [5 Missing :warning: ](https://app.codecov.io/gh/opensumi/core/pull/4476?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi) |
| [...i-native/src/browser/chat/chat.feature.registry.ts](https://app.codecov.io/gh/opensumi/core/pull/4476?src=pr&el=tree&filepath=packages%2Fai-native%2Fsrc%2Fbrowser%2Fchat%2Fchat.feature.registry.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi#diff-cGFja2FnZXMvYWktbmF0aXZlL3NyYy9icm93c2VyL2NoYXQvY2hhdC5mZWF0dXJlLnJlZ2lzdHJ5LnRz) | 0.00% | [4 Missing :warning: ](https://app.codecov.io/gh/opensumi/core/pull/4476?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi) |
| [...ai-native/src/browser/chat/chat-manager.service.ts](https://app.codecov.io/gh/opensumi/core/pull/4476?src=pr&el=tree&filepath=packages%2Fai-native%2Fsrc%2Fbrowser%2Fchat%2Fchat-manager.service.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi#diff-cGFja2FnZXMvYWktbmF0aXZlL3NyYy9icm93c2VyL2NoYXQvY2hhdC1tYW5hZ2VyLnNlcnZpY2UudHM=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/opensumi/core/pull/4476?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi) |
| [...i-native/src/browser/chat/chat.internal.service.ts](https://app.codecov.io/gh/opensumi/core/pull/4476?src=pr&el=tree&filepath=packages%2Fai-native%2Fsrc%2Fbrowser%2Fchat%2Fchat.internal.service.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi#diff-cGFja2FnZXMvYWktbmF0aXZlL3NyYy9icm93c2VyL2NoYXQvY2hhdC5pbnRlcm5hbC5zZXJ2aWNlLnRz) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/opensumi/core/pull/4476?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi) |
| [...ai-native/src/browser/model/msg-history-manager.ts](https://app.codecov.io/gh/opensumi/core/pull/4476?src=pr&el=tree&filepath=packages%2Fai-native%2Fsrc%2Fbrowser%2Fmodel%2Fmsg-history-manager.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi#diff-cGFja2FnZXMvYWktbmF0aXZlL3NyYy9icm93c2VyL21vZGVsL21zZy1oaXN0b3J5LW1hbmFnZXIudHM=) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/opensumi/core/pull/4476?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #4476      +/-   ##
==========================================
- Coverage   53.19%   53.15%   -0.05%     
==========================================
  Files        1665     1665              
  Lines      102749   102628     -121     
  Branches    22228    22213      -15     
==========================================
- Hits        54654    54548     -106     
+ Misses      40012    39998      -14     
+ Partials     8083     8082       -1     
```

| [Flag](https://app.codecov.io/gh/opensumi/core/pull/4476/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi) | Coverage Δ | |
|---|---|---|
| [jsdom](https://app.codecov.io/gh/opensumi/core/pull/4476/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi) | `48.63% <4.54%> (-0.05%)` | :arrow_down: |
| [node](https://app.codecov.io/gh/opensumi/core/pull/4476/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi) | `12.13% <0.00%> (-0.08%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/opensumi/core/pull/4476?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensumi).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE