@lobehub/lobehub 2.0.0-next.109 → 2.0.0-next.110

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/apps/desktop/src/common/routes.ts +0 -6
  3. package/apps/desktop/src/main/appBrowsers.ts +0 -13
  4. package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +29 -48
  5. package/apps/desktop/src/main/controllers/__tests__/BrowserWindowsCtr.test.ts +21 -72
  6. package/apps/desktop/src/main/core/browser/Browser.ts +1 -0
  7. package/apps/desktop/src/main/core/browser/BrowserManager.ts +1 -56
  8. package/apps/desktop/src/main/menus/impls/macOS.ts +9 -3
  9. package/changelog/v1.json +9 -0
  10. package/locales/ar/setting.json +7 -1
  11. package/locales/bg-BG/setting.json +7 -1
  12. package/locales/de-DE/setting.json +7 -1
  13. package/locales/en-US/setting.json +7 -1
  14. package/locales/es-ES/setting.json +7 -1
  15. package/locales/fa-IR/setting.json +7 -1
  16. package/locales/fr-FR/setting.json +7 -1
  17. package/locales/it-IT/setting.json +7 -1
  18. package/locales/ja-JP/setting.json +7 -1
  19. package/locales/ko-KR/setting.json +7 -1
  20. package/locales/nl-NL/setting.json +7 -1
  21. package/locales/pl-PL/setting.json +7 -1
  22. package/locales/pt-BR/setting.json +7 -1
  23. package/locales/ru-RU/setting.json +7 -1
  24. package/locales/tr-TR/setting.json +7 -1
  25. package/locales/vi-VN/setting.json +7 -1
  26. package/locales/zh-CN/setting.json +6 -0
  27. package/locales/zh-TW/setting.json +7 -1
  28. package/package.json +1 -1
  29. package/packages/const/src/settings/common.ts +1 -0
  30. package/packages/model-bank/src/aiModels/ollamacloud.ts +0 -1
  31. package/packages/types/src/user/settings/general.ts +3 -0
  32. package/src/app/[variants]/(main)/chat/components/topic/features/Topic/TopicListContent/TopicItem/index.tsx +32 -18
  33. package/src/app/[variants]/(main)/layouts/desktop/DesktopLayoutContainer.tsx +3 -6
  34. package/src/app/[variants]/(main)/layouts/desktop/SideBar/PinList/index.tsx +21 -14
  35. package/src/app/[variants]/(main)/settings/common/features/Common/Common.tsx +23 -1
  36. package/src/features/ChatItem/components/MessageContent.tsx +2 -1
  37. package/src/features/ChatList/Messages/Assistant/Actions/index.tsx +1 -0
  38. package/src/features/ChatList/Messages/Assistant/index.tsx +1 -1
  39. package/src/features/ChatList/Messages/Default.tsx +2 -0
  40. package/src/features/ChatList/Messages/index.tsx +80 -31
  41. package/src/features/ChatList/components/ContextMenu.tsx +391 -0
  42. package/src/features/ChatList/hooks/useChatItemContextMenu.tsx +135 -0
  43. package/src/locales/default/setting.ts +6 -0
  44. package/src/store/user/slices/settings/selectors/general.ts +8 -0
@@ -293,6 +293,12 @@
293
293
  "elegant": "Elegancki",
294
294
  "title": "Animacja reakcji"
295
295
  },
296
+ "contextMenuMode": {
297
+ "default": "Domyślnie",
298
+ "desc": "Wybierz sposób wyświetlania menu kontekstowego wiadomości czatu",
299
+ "disabled": "Nie używaj",
300
+ "title": "Schemat menu kontekstowego"
301
+ },
296
302
  "neutralColor": {
297
303
  "desc": "Dostosowanie odcieni szarości w różnych kolorach",
298
304
  "title": "Kolor neutralny"
@@ -777,4 +783,4 @@
777
783
  },
778
784
  "title": "Narzędzia rozszerzeń"
779
785
  }
780
- }
786
+ }
@@ -293,6 +293,12 @@
293
293
  "elegant": "Elegante",
294
294
  "title": "Animação de Resposta"
295
295
  },
296
+ "contextMenuMode": {
297
+ "default": "Padrão",
298
+ "desc": "Escolha o modo de exibição do menu de contexto ao clicar com o botão direito nas mensagens de chat",
299
+ "disabled": "Desativado",
300
+ "title": "Modo do menu de contexto"
301
+ },
296
302
  "neutralColor": {
297
303
  "desc": "Personalização de escala de cinza com diferentes inclinações de cor",
298
304
  "title": "Cor Neutra"
@@ -777,4 +783,4 @@
777
783
  },
778
784
  "title": "Ferramentas de Extensão"
779
785
  }
780
- }
786
+ }
@@ -293,6 +293,12 @@
293
293
  "elegant": "Элегантный",
294
294
  "title": "Анимация отклика"
295
295
  },
296
+ "contextMenuMode": {
297
+ "default": "По умолчанию",
298
+ "desc": "Выберите способ отображения контекстного меню сообщений чата",
299
+ "disabled": "Не использовать",
300
+ "title": "Схема контекстного меню"
301
+ },
296
302
  "neutralColor": {
297
303
  "desc": "Настройка градаций серого с различными цветовыми наклонами",
298
304
  "title": "Нейтральный цвет"
@@ -777,4 +783,4 @@
777
783
  },
778
784
  "title": "Дополнительные инструменты"
779
785
  }
780
- }
786
+ }
@@ -293,6 +293,12 @@
293
293
  "elegant": "Zarif",
294
294
  "title": "Yanıt Animasyonu"
295
295
  },
296
+ "contextMenuMode": {
297
+ "default": "Varsayılan",
298
+ "desc": "Sohbet mesajları sağ tıklama menüsünün görüntüleme seçeneğini seçin",
299
+ "disabled": "Kullanma",
300
+ "title": "Sağ Tıklama Menüsü Seçeneği"
301
+ },
296
302
  "neutralColor": {
297
303
  "desc": "Farklı renk eğilimlerine sahip gri tonları özelleştirme",
298
304
  "title": "Nötr Renk"
@@ -777,4 +783,4 @@
777
783
  },
778
784
  "title": "Uzantı Araçları"
779
785
  }
780
- }
786
+ }
@@ -293,6 +293,12 @@
293
293
  "elegant": "Thanh lịch",
294
294
  "title": "Hoạt ảnh phản hồi"
295
295
  },
296
+ "contextMenuMode": {
297
+ "default": "Mặc định",
298
+ "desc": "Chọn phương án hiển thị menu chuột phải cho tin nhắn trò chuyện",
299
+ "disabled": "Không sử dụng",
300
+ "title": "Phương án menu chuột phải"
301
+ },
296
302
  "neutralColor": {
297
303
  "desc": "Tùy chỉnh thang độ xám với các xu hướng màu sắc khác nhau",
298
304
  "title": "Màu trung tính"
@@ -777,4 +783,4 @@
777
783
  },
778
784
  "title": "Công cụ mở rộng"
779
785
  }
780
- }
786
+ }
@@ -293,6 +293,12 @@
293
293
  "elegant": "优雅",
294
294
  "title": "响应动画"
295
295
  },
296
+ "contextMenuMode": {
297
+ "default": "默认",
298
+ "desc": "选择聊天消息右键菜单的显示方案",
299
+ "disabled": "不使用",
300
+ "title": "右键菜单方案"
301
+ },
296
302
  "neutralColor": {
297
303
  "desc": "不同色彩倾向的灰阶自定义",
298
304
  "title": "中性色"
@@ -293,6 +293,12 @@
293
293
  "elegant": "優雅",
294
294
  "title": "回應動畫"
295
295
  },
296
+ "contextMenuMode": {
297
+ "default": "預設",
298
+ "desc": "選擇聊天訊息右鍵選單的顯示方案",
299
+ "disabled": "不使用",
300
+ "title": "右鍵選單方案"
301
+ },
296
302
  "neutralColor": {
297
303
  "desc": "不同色彩傾向的灰階自訂",
298
304
  "title": "中性色"
@@ -777,4 +783,4 @@
777
783
  },
778
784
  "title": "擴展工具"
779
785
  }
780
- }
786
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.109",
3
+ "version": "2.0.0-next.110",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -2,6 +2,7 @@ import { UserGeneralConfig } from '@lobechat/types';
2
2
 
3
3
  export const DEFAULT_COMMON_SETTINGS: UserGeneralConfig = {
4
4
  animationMode: 'agile',
5
+ // contextMenuMode not set default value, use env to calc
5
6
  fontSize: 14,
6
7
  highlighterTheme: 'lobe-theme',
7
8
  mermaidTheme: 'lobe-theme',
@@ -23,7 +23,6 @@ const ollamaCloudModels: AIChatModelCard[] = [
23
23
  description:
24
24
  'Gemini 3 Pro 是 Google 最智能的模型,具有 SOTA 推理和多模式理解,以及强大的代理和氛围编码功能。',
25
25
  displayName: 'Gemini 3 Pro Preview',
26
- enabled: true,
27
26
  id: 'gemini-3-pro-preview',
28
27
  releasedAt: '2025-11-20',
29
28
  type: 'chat',
@@ -4,8 +4,11 @@ import type { ResponseAnimationStyle } from '../../aiProvider';
4
4
 
5
5
  export type AnimationMode = 'disabled' | 'agile' | 'elegant';
6
6
 
7
+ export type ContextMenuMode = 'disabled' | 'default';
8
+
7
9
  export interface UserGeneralConfig {
8
10
  animationMode?: AnimationMode;
11
+ contextMenuMode?: ContextMenuMode;
9
12
  fontSize: number;
10
13
  highlighterTheme?: HighlighterProps['theme'];
11
14
  mermaidTheme?: MermaidProps['theme'];
@@ -1,10 +1,13 @@
1
1
  import { Skeleton } from 'antd';
2
2
  import { createStyles } from 'antd-style';
3
+ import qs from 'query-string';
3
4
  import { Suspense, memo, useState } from 'react';
4
5
  import { Flexbox } from 'react-layout-kit';
6
+ import { Link } from 'react-router-dom';
5
7
 
6
8
  import { useChatStore } from '@/store/chat';
7
9
  import { useGlobalStore } from '@/store/global';
10
+ import { useSessionStore } from '@/store/session';
8
11
 
9
12
  import ThreadList from '../ThreadList';
10
13
  import DefaultContent from './DefaultContent';
@@ -52,32 +55,43 @@ const TopicItem = memo<ConfigCellProps>(({ title, active, id, fav, threadId }) =
52
55
  const { styles, cx } = useStyles();
53
56
  const toggleConfig = useGlobalStore((s) => s.toggleMobileTopic);
54
57
  const [toggleTopic] = useChatStore((s) => [s.switchTopic]);
58
+ const activeId = useSessionStore((s) => s.activeId);
55
59
  const [isHover, setHovering] = useState(false);
56
60
 
61
+ const topicUrl = qs.stringifyUrl({
62
+ query: id ? { session: activeId, topic: id } : { session: activeId },
63
+ url: '/chat',
64
+ });
65
+
57
66
  return (
58
67
  <Flexbox style={{ position: 'relative' }}>
59
- <Flexbox
60
- align={'center'}
61
- className={cx(styles.container, 'topic-item', active && !threadId && styles.active)}
62
- distribution={'space-between'}
63
- horizontal
64
- onClick={() => {
68
+ <Link
69
+ onClick={(e) => {
70
+ e.preventDefault();
65
71
  toggleTopic(id);
66
72
  toggleConfig(false);
67
73
  }}
68
- onMouseEnter={() => {
69
- setHovering(true);
70
- }}
71
- onMouseLeave={() => {
72
- setHovering(false);
73
- }}
74
+ to={topicUrl}
74
75
  >
75
- {!id ? (
76
- <DefaultContent />
77
- ) : (
78
- <TopicContent fav={fav} id={id} showMore={isHover} title={title} />
79
- )}
80
- </Flexbox>
76
+ <Flexbox
77
+ align={'center'}
78
+ className={cx(styles.container, 'topic-item', active && !threadId && styles.active)}
79
+ distribution={'space-between'}
80
+ horizontal
81
+ onMouseEnter={() => {
82
+ setHovering(true);
83
+ }}
84
+ onMouseLeave={() => {
85
+ setHovering(false);
86
+ }}
87
+ >
88
+ {!id ? (
89
+ <DefaultContent />
90
+ ) : (
91
+ <TopicContent fav={fav} id={id} showMore={isHover} title={title} />
92
+ )}
93
+ </Flexbox>
94
+ </Link>
81
95
  {active && (
82
96
  <Suspense
83
97
  fallback={
@@ -1,25 +1,22 @@
1
1
  import { useTheme } from 'antd-style';
2
2
  import { PropsWithChildren, Suspense, memo } from 'react';
3
3
  import { Flexbox } from 'react-layout-kit';
4
- import { useLocation } from 'react-router-dom';
5
4
 
6
5
  import SideBar from './SideBar';
7
6
 
8
7
  const DesktopLayoutContainer = memo<PropsWithChildren>(({ children }) => {
9
8
  const theme = useTheme();
10
- const location = useLocation();
11
- const pathname = location.pathname;
12
- const hideSideBar = pathname.startsWith('/settings');
9
+
13
10
  return (
14
11
  <>
15
12
  <Suspense>
16
- {!hideSideBar && <SideBar />}
13
+ <SideBar />
17
14
  </Suspense>
18
15
  <Flexbox
19
16
  style={{
20
17
  background: theme.colorBgLayout,
21
18
  borderInlineStart: `1px solid ${theme.colorBorderSecondary}`,
22
- borderStartStartRadius: !hideSideBar ? 12 : undefined,
19
+ borderStartStartRadius: 12,
23
20
  borderTop: `1px solid ${theme.colorBorderSecondary}`,
24
21
  overflow: 'hidden',
25
22
  }}
@@ -3,7 +3,9 @@ import { Divider } from 'antd';
3
3
  import { createStyles } from 'antd-style';
4
4
  import isEqual from 'fast-deep-equal';
5
5
  import { Flexbox } from 'react-layout-kit';
6
+ import { Link } from 'react-router-dom';
6
7
 
8
+ import { SESSION_CHAT_URL } from '@/const/url';
7
9
  import { usePinnedAgentState } from '@/hooks/usePinnedAgentState';
8
10
  import { useSwitchSession } from '@/hooks/useSwitchSession';
9
11
  import { useSessionStore } from '@/store/session';
@@ -93,21 +95,26 @@ const PinList = () => {
93
95
  placement={'right'}
94
96
  title={sessionHelpers.getTitle(item.meta)}
95
97
  >
96
- <Flexbox
97
- className={cx(
98
- styles.ink,
99
- isPinned && activeId === item.id ? styles.inkActive : undefined,
100
- )}
98
+ <Link
99
+ onClick={(e) => {
100
+ e.preventDefault();
101
+ switchAgent(item.id);
102
+ }}
103
+ to={SESSION_CHAT_URL(item.id)}
101
104
  >
102
- <Avatar
103
- avatar={sessionHelpers.getAvatar(item.meta)}
104
- background={item.meta.backgroundColor}
105
- onClick={() => {
106
- switchAgent(item.id);
107
- }}
108
- size={40}
109
- />
110
- </Flexbox>
105
+ <Flexbox
106
+ className={cx(
107
+ styles.ink,
108
+ isPinned && activeId === item.id ? styles.inkActive : undefined,
109
+ )}
110
+ >
111
+ <Avatar
112
+ avatar={sessionHelpers.getAvatar(item.meta)}
113
+ background={item.meta.backgroundColor}
114
+ size={40}
115
+ />
116
+ </Flexbox>
117
+ </Link>
111
118
  </Tooltip>
112
119
  </Flexbox>
113
120
  ))}
@@ -4,7 +4,7 @@ import { Form, type FormGroupItemType, Icon, ImageSelect, InputPassword } from '
4
4
  import { Select } from '@lobehub/ui';
5
5
  import { Segmented, Skeleton } from 'antd';
6
6
  import isEqual from 'fast-deep-equal';
7
- import { Ban, Gauge, Loader2Icon, Monitor, Moon, Sun, Waves } from 'lucide-react';
7
+ import { Ban, Gauge, Loader2Icon, Monitor, Moon, Mouse, Sun, Waves } from 'lucide-react';
8
8
  import { memo, useState } from 'react';
9
9
  import { useTranslation } from 'react-i18next';
10
10
 
@@ -113,6 +113,28 @@ const Common = memo(() => {
113
113
  minWidth: undefined,
114
114
  name: 'animationMode',
115
115
  },
116
+ {
117
+ children: (
118
+ <Segmented
119
+ options={[
120
+ {
121
+ icon: <Icon icon={Ban} size={16} />,
122
+ label: t('settingAppearance.contextMenuMode.disabled'),
123
+ value: 'disabled',
124
+ },
125
+ {
126
+ icon: <Icon icon={Mouse} size={16} />,
127
+ label: t('settingAppearance.contextMenuMode.default'),
128
+ value: 'default',
129
+ },
130
+ ]}
131
+ />
132
+ ),
133
+ desc: t('settingAppearance.contextMenuMode.desc'),
134
+ label: t('settingAppearance.contextMenuMode.title'),
135
+ minWidth: undefined,
136
+ name: 'contextMenuMode',
137
+ },
116
138
 
117
139
  {
118
140
  children: (
@@ -5,6 +5,7 @@ import { type ReactNode, memo, useMemo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import { Flexbox } from 'react-layout-kit';
7
7
 
8
+ import { MessageContentClassName } from '@/features/ChatList/Messages/Default';
8
9
  import { useChatStore } from '@/store/chat';
9
10
  import { useUserStore } from '@/store/user';
10
11
  import { userGeneralSettingsSelectors } from '@/store/user/selectors';
@@ -83,7 +84,7 @@ const MessageContent = memo<MessageContentProps>(
83
84
 
84
85
  return (
85
86
  <Flexbox
86
- className={cx(styles.message, editing && styles.editingContainer, className)}
87
+ className={cx(styles.message, className, editing && styles.editingContainer, MessageContentClassName)}
87
88
  onDoubleClick={onDoubleClick}
88
89
  >
89
90
  {messageContent}
@@ -21,6 +21,7 @@ interface AssistantActionsProps {
21
21
  id: string;
22
22
  index: number;
23
23
  }
24
+
24
25
  export const AssistantActionsBar = memo<AssistantActionsProps>(({ id, data, index }) => {
25
26
  const { error, tools } = data;
26
27
  const [isThreadMode, hasThread, isRegenerating, isCollapsed] = useChatStore((s) => [
@@ -75,7 +75,7 @@ export const useStyles = createStyles(
75
75
  justify-content: ${placement === 'left' ? 'flex-end' : 'flex-start'};
76
76
  `,
77
77
  editing &&
78
- css`
78
+ css`
79
79
  pointer-events: none !important;
80
80
  opacity: 0 !important;
81
81
  `,
@@ -6,6 +6,8 @@ import { LOADING_FLAT } from '@/const/message';
6
6
  import { useChatStore } from '@/store/chat';
7
7
  import { messageStateSelectors } from '@/store/chat/selectors';
8
8
 
9
+ export const MessageContentClassName = 'msg_content_flag'
10
+
9
11
  export const DefaultMessage = memo<
10
12
  UIChatMessage & {
11
13
  addIdOnDOM?: boolean;
@@ -3,24 +3,28 @@
3
3
  import { isDesktop } from '@lobechat/const';
4
4
  import { createStyles } from 'antd-style';
5
5
  import isEqual from 'fast-deep-equal';
6
- import { ReactNode, memo, useCallback, useEffect, useMemo, useRef } from 'react';
6
+ import { useSearchParams } from 'next/navigation';
7
+ import { MouseEvent, ReactNode, memo, use, useCallback, useEffect, useRef } from 'react';
7
8
  import { Flexbox } from 'react-layout-kit';
8
9
 
9
- import {
10
- removeVirtuaVisibleItem,
11
- upsertVirtuaVisibleItem,
12
- } from '@/features/ChatList/components/VirtualizedList/VirtuosoContext';
13
- import { getChatStoreState, useChatStore } from '@/store/chat';
14
- import { displayMessageSelectors, messageStateSelectors } from '@/store/chat/selectors';
15
-
10
+ import ContextMenu from '../components/ContextMenu';
16
11
  import History from '../components/History';
17
12
  import { InPortalThreadContext } from '../context/InPortalThreadContext';
13
+ import { useChatItemContextMenu } from '../hooks/useChatItemContextMenu';
18
14
  import AssistantMessage from './Assistant';
19
15
  import GroupMessage from './Group';
20
16
  import SupervisorMessage from './Supervisor';
21
17
  import ToolMessage from './Tool';
22
18
  import UserMessage from './User';
23
19
 
20
+ import {
21
+ VirtuaContext,
22
+ removeVirtuaVisibleItem,
23
+ upsertVirtuaVisibleItem,
24
+ } from '@/features/ChatList/components/VirtualizedList/VirtuosoContext';
25
+ import { getChatStoreState, useChatStore } from '@/store/chat';
26
+ import { displayMessageSelectors, messageStateSelectors } from '@/store/chat/selectors';
27
+
24
28
  const useStyles = createStyles(({ css, prefixCls }) => ({
25
29
  loading: css`
26
30
  opacity: 0.6;
@@ -58,17 +62,41 @@ const Item = memo<ChatListItemProps>(
58
62
  }) => {
59
63
  const { styles, cx } = useStyles();
60
64
  const containerRef = useRef<HTMLDivElement | null>(null);
65
+ const virtuaRef = use(VirtuaContext);
66
+ const searchParams = useSearchParams();
67
+ const topic = searchParams.get('topic');
68
+
69
+ const [role, editing, isMessageCreating] = useChatStore(
70
+ (s) => {
71
+ const item = displayMessageSelectors.getDisplayMessageById(id)(s);
72
+ return [
73
+ item?.role,
74
+ messageStateSelectors.isMessageEditing(id)(s),
75
+ messageStateSelectors.isMessageCreating(id)(s),
76
+ ];
77
+ },
78
+ isEqual,
79
+ );
61
80
 
62
- const [role, isMessageCreating] = useChatStore((s) => [
63
- displayMessageSelectors.getDisplayMessageById(id)(s)?.role,
64
- messageStateSelectors.isMessageCreating(id)(s),
65
- ]);
81
+ const {
82
+ containerRef: contextMenuContainerRef,
83
+ contextMenuState,
84
+ handleContextMenu,
85
+ hideContextMenu,
86
+ } = useChatItemContextMenu({
87
+ editing,
88
+ onActionClick: () => {},
89
+ });
90
+
91
+ const setContainerRef = useCallback(
92
+ (node: HTMLDivElement | null) => {
93
+ containerRef.current = node;
94
+ contextMenuContainerRef.current = node;
95
+ },
96
+ [contextMenuContainerRef],
97
+ );
66
98
 
67
99
  // ======================= Performance Optimization ======================= //
68
- // these useMemo/useCallback are all for the performance optimization
69
- // maybe we can remove it in React 19
70
- // ======================================================================== //
71
-
72
100
  useEffect(() => {
73
101
  if (typeof window === 'undefined' || typeof IntersectionObserver === 'undefined') return;
74
102
 
@@ -107,22 +135,32 @@ const Item = memo<ChatListItemProps>(
107
135
  };
108
136
  }, [index]);
109
137
 
110
- const onContextMenu = useCallback(async () => {
111
- const item = displayMessageSelectors.getDisplayMessageById(id)(getChatStoreState());
138
+ const onContextMenu = useCallback(
139
+ async (event: MouseEvent<HTMLDivElement>) => {
140
+ if (!role || (role !== 'user' && role !== 'assistant')) return;
112
141
 
113
- if (isDesktop && item) {
114
- const { electronSystemService } = await import('@/services/electron/system');
142
+ const item = displayMessageSelectors.getDisplayMessageById(id)(getChatStoreState());
143
+ if (!item) return;
115
144
 
116
- electronSystemService.showContextMenu('chat', {
117
- content: item.content,
118
- hasError: !!item.error,
119
- messageId: id,
120
- role: item.role,
121
- });
122
- }
123
- }, [id]);
145
+ if (isDesktop) {
146
+ const { electronSystemService } = await import('@/services/electron/system');
147
+
148
+ electronSystemService.showContextMenu('chat', {
149
+ content: item.content,
150
+ hasError: !!item.error,
151
+ messageId: id,
152
+ role: item.role,
153
+ });
154
+
155
+ return;
156
+ }
157
+
158
+ handleContextMenu(event);
159
+ },
160
+ [handleContextMenu, id, role],
161
+ );
124
162
 
125
- const renderContent = useMemo(() => {
163
+ const renderContent = useCallback(() => {
126
164
  switch (role) {
127
165
  case 'user': {
128
166
  return <UserMessage disableEditing={disableEditing} id={id} index={index} />;
@@ -171,11 +209,22 @@ const Item = memo<ChatListItemProps>(
171
209
  className={cx(styles.message, className, isMessageCreating && styles.loading)}
172
210
  data-index={index}
173
211
  onContextMenu={onContextMenu}
174
- ref={containerRef}
212
+ ref={setContainerRef}
175
213
  >
176
- {renderContent}
214
+ {renderContent()}
177
215
  {endRender}
178
216
  </Flexbox>
217
+ <ContextMenu
218
+ id={id}
219
+ inPortalThread={inPortalThread}
220
+ index={index}
221
+ onClose={hideContextMenu}
222
+ position={contextMenuState.position}
223
+ selectedText={contextMenuState.selectedText}
224
+ topic={topic}
225
+ virtuaRef={virtuaRef}
226
+ visible={contextMenuState.visible}
227
+ />
179
228
  </InPortalThreadContext.Provider>
180
229
  );
181
230
  },