@lobehub/lobehub 2.0.0-next.280 → 2.0.0-next.282

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 (42) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/e2e/src/steps/agent/conversation-mgmt.steps.ts +47 -3
  4. package/package.json +1 -1
  5. package/packages/builtin-tool-group-agent-builder/src/client/Render/BatchCreateAgents.tsx +19 -3
  6. package/packages/builtin-tool-group-agent-builder/src/client/Streaming/BatchCreateAgents/index.tsx +19 -4
  7. package/packages/builtin-tool-group-agent-builder/src/client/Streaming/UpdateAgentPrompt/index.tsx +3 -13
  8. package/packages/builtin-tool-group-agent-builder/src/client/Streaming/UpdateGroupPrompt/index.tsx +1 -1
  9. package/packages/builtin-tool-group-agent-builder/src/systemRole.ts +83 -121
  10. package/packages/builtin-tool-local-system/src/client/Inspector/ReadLocalFile/index.tsx +20 -2
  11. package/packages/observability-otel/src/node.ts +40 -3
  12. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/List/Item/index.tsx +9 -1
  13. package/src/app/[variants]/(main)/agent/features/Conversation/ConversationArea.tsx +1 -28
  14. package/src/app/[variants]/(main)/community/(detail)/features/MakedownRender.tsx +3 -3
  15. package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/GroupMember.tsx +8 -1
  16. package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/Header/Avatar.tsx +2 -13
  17. package/src/app/[variants]/(main)/group/_layout/Sidebar/Header/Agent/index.tsx +3 -4
  18. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/List/Item/index.tsx +17 -8
  19. package/src/app/[variants]/(main)/group/features/Conversation/ConversationArea.tsx +1 -29
  20. package/src/app/[variants]/(main)/group/features/Conversation/Header/ShareButton/index.tsx +0 -2
  21. package/src/app/[variants]/(main)/group/features/GroupAvatar.tsx +17 -9
  22. package/src/app/[variants]/(main)/group/profile/features/AgentBuilder/TopicSelector.tsx +8 -5
  23. package/src/app/[variants]/(main)/group/profile/features/Header/ChromeTabs/index.tsx +20 -2
  24. package/src/app/[variants]/(main)/group/profile/features/MemberProfile/AgentTool.tsx +4 -2
  25. package/src/app/[variants]/(main)/group/profile/features/ProfileHydration.tsx +5 -25
  26. package/src/features/AgentGroupAvatar/index.tsx +38 -0
  27. package/src/features/Conversation/Messages/Supervisor/index.tsx +8 -2
  28. package/src/features/Conversation/Messages/User/useMarkdown.tsx +1 -2
  29. package/src/features/EditorModal/EditorCanvas.tsx +62 -0
  30. package/src/features/EditorModal/TextArea.tsx +30 -0
  31. package/src/features/EditorModal/Typobar.tsx +139 -0
  32. package/src/features/EditorModal/index.tsx +18 -8
  33. package/src/features/NavPanel/components/EmptyNavItem.tsx +2 -2
  34. package/src/features/NavPanel/components/NavItem.tsx +27 -3
  35. package/src/features/ToolTag/index.tsx +167 -0
  36. package/src/server/routers/lambda/topic.ts +8 -1
  37. package/src/services/chat/mecha/contextEngineering.test.ts +1 -1
  38. package/src/services/chat/mecha/contextEngineering.ts +3 -4
  39. package/src/services/chat/mecha/memoryManager.ts +9 -38
  40. package/src/store/agentGroup/initialState.ts +1 -1
  41. package/src/store/agentGroup/slices/lifecycle.ts +15 -2
  42. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/hooks/useTopicNavigation.ts +0 -49
@@ -0,0 +1,62 @@
1
+ import {
2
+ IEditor,
3
+ ReactCodePlugin,
4
+ ReactCodemirrorPlugin,
5
+ ReactHRPlugin,
6
+ ReactLinkPlugin,
7
+ ReactListPlugin,
8
+ ReactMathPlugin,
9
+ ReactTablePlugin,
10
+ } from '@lobehub/editor';
11
+ import { Editor } from '@lobehub/editor/react';
12
+ import { Flexbox } from '@lobehub/ui';
13
+ import { FC } from 'react';
14
+
15
+ import TypoBar from './Typobar';
16
+
17
+ interface EditorCanvasProps {
18
+ defaultValue?: string;
19
+ editor?: IEditor;
20
+ }
21
+
22
+ const EditorCanvas: FC<EditorCanvasProps> = ({ defaultValue, editor }) => {
23
+ return (
24
+ <>
25
+ <TypoBar editor={editor} />
26
+ <Flexbox
27
+ padding={16}
28
+ style={{ cursor: 'text', maxHeight: '80vh', minHeight: '50vh', overflowY: 'auto' }}
29
+ >
30
+ <Editor
31
+ autoFocus
32
+ content={''}
33
+ editor={editor}
34
+ onInit={(editor) => {
35
+ if (!editor || !defaultValue) return;
36
+ try {
37
+ editor?.setDocument('markdown', defaultValue);
38
+ } catch (e) {
39
+ console.error('setDocument error:', e);
40
+ }
41
+ }}
42
+ plugins={[
43
+ ReactListPlugin,
44
+ ReactCodePlugin,
45
+ ReactCodemirrorPlugin,
46
+ ReactHRPlugin,
47
+ ReactLinkPlugin,
48
+ ReactTablePlugin,
49
+ ReactMathPlugin,
50
+ ]}
51
+ style={{
52
+ paddingBottom: 120,
53
+ }}
54
+ type={'text'}
55
+ variant={'chat'}
56
+ />
57
+ </Flexbox>
58
+ </>
59
+ );
60
+ };
61
+
62
+ export default EditorCanvas;
@@ -0,0 +1,30 @@
1
+ import { TextArea } from '@lobehub/ui';
2
+ import { FC } from 'react';
3
+
4
+ interface EditorCanvasProps {
5
+ defaultValue?: string;
6
+ onChange?: (value: string) => void;
7
+ value?: string;
8
+ }
9
+
10
+ const EditorCanvas: FC<EditorCanvasProps> = ({ defaultValue, value, onChange }) => {
11
+ return (
12
+ <TextArea
13
+ defaultValue={defaultValue}
14
+ onChange={(e) => {
15
+ onChange?.(e.target.value);
16
+ }}
17
+ style={{
18
+ cursor: 'text',
19
+ maxHeight: '80vh',
20
+ minHeight: '50vh',
21
+ overflowY: 'auto',
22
+ padding: 16,
23
+ }}
24
+ value={value}
25
+ variant={'borderless'}
26
+ />
27
+ );
28
+ };
29
+
30
+ export default EditorCanvas;
@@ -0,0 +1,139 @@
1
+ import { HotkeyEnum, type IEditor, getHotkeyById } from '@lobehub/editor';
2
+ import { useEditorState } from '@lobehub/editor/react';
3
+ import {
4
+ ChatInputActionBar,
5
+ ChatInputActions,
6
+ type ChatInputActionsProps,
7
+ } from '@lobehub/editor/react';
8
+ import { cssVar } from 'antd-style';
9
+ import {
10
+ BoldIcon,
11
+ CodeXmlIcon,
12
+ ItalicIcon,
13
+ ListIcon,
14
+ ListOrderedIcon,
15
+ ListTodoIcon,
16
+ MessageSquareQuote,
17
+ SigmaIcon,
18
+ SquareDashedBottomCodeIcon,
19
+ StrikethroughIcon,
20
+ UnderlineIcon,
21
+ } from 'lucide-react';
22
+ import { memo, useMemo } from 'react';
23
+ import { useTranslation } from 'react-i18next';
24
+
25
+ const TypoBar = memo<{ editor?: IEditor }>(({ editor }) => {
26
+ const { t } = useTranslation('editor');
27
+ const editorState = useEditorState(editor);
28
+
29
+ const items: ChatInputActionsProps['items'] = useMemo(
30
+ () =>
31
+ [
32
+ {
33
+ active: editorState.isBold,
34
+ icon: BoldIcon,
35
+ key: 'bold',
36
+ label: t('typobar.bold'),
37
+ onClick: editorState.bold,
38
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Bold).keys },
39
+ },
40
+ {
41
+ active: editorState.isItalic,
42
+ icon: ItalicIcon,
43
+ key: 'italic',
44
+ label: t('typobar.italic'),
45
+ onClick: editorState.italic,
46
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Italic).keys },
47
+ },
48
+ {
49
+ active: editorState.isUnderline,
50
+ icon: UnderlineIcon,
51
+ key: 'underline',
52
+ label: t('typobar.underline'),
53
+ onClick: editorState.underline,
54
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Underline).keys },
55
+ },
56
+ {
57
+ active: editorState.isStrikethrough,
58
+ icon: StrikethroughIcon,
59
+ key: 'strikethrough',
60
+ label: t('typobar.strikethrough'),
61
+ onClick: editorState.strikethrough,
62
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Strikethrough).keys },
63
+ },
64
+ {
65
+ type: 'divider',
66
+ },
67
+
68
+ {
69
+ icon: ListIcon,
70
+ key: 'bulletList',
71
+ label: t('typobar.bulletList'),
72
+ onClick: editorState.bulletList,
73
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.BulletList).keys },
74
+ },
75
+ {
76
+ icon: ListOrderedIcon,
77
+ key: 'numberlist',
78
+ label: t('typobar.numberList'),
79
+ onClick: editorState.numberList,
80
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.NumberList).keys },
81
+ },
82
+ {
83
+ icon: ListTodoIcon,
84
+ key: 'tasklist',
85
+ label: t('typobar.taskList'),
86
+ onClick: editorState.checkList,
87
+ },
88
+ {
89
+ type: 'divider',
90
+ },
91
+ {
92
+ active: editorState.isBlockquote,
93
+ icon: MessageSquareQuote,
94
+ key: 'blockquote',
95
+ label: t('typobar.blockquote'),
96
+ onClick: editorState.blockquote,
97
+ },
98
+ {
99
+ type: 'divider',
100
+ },
101
+ {
102
+ icon: SigmaIcon,
103
+ key: 'math',
104
+ label: t('typobar.tex'),
105
+ onClick: editorState.insertMath,
106
+ },
107
+ {
108
+ active: editorState.isCode,
109
+ icon: CodeXmlIcon,
110
+ key: 'code',
111
+ label: t('typobar.code'),
112
+ onClick: editorState.code,
113
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.CodeInline).keys },
114
+ },
115
+ {
116
+ icon: SquareDashedBottomCodeIcon,
117
+ key: 'codeblock',
118
+ label: t('typobar.codeblock'),
119
+ onClick: editorState.codeblock,
120
+ },
121
+ ].filter(Boolean) as ChatInputActionsProps['items'],
122
+ [editorState],
123
+ );
124
+
125
+ return (
126
+ <ChatInputActionBar
127
+ left={<ChatInputActions items={items} />}
128
+ style={{
129
+ background: cssVar.colorFillQuaternary,
130
+ borderTopLeftRadius: 8,
131
+ borderTopRightRadius: 8,
132
+ }}
133
+ />
134
+ );
135
+ });
136
+
137
+ TypoBar.displayName = 'TypoBar';
138
+
139
+ export default TypoBar;
@@ -3,7 +3,11 @@ import { Modal, ModalProps, createRawModal } from '@lobehub/ui';
3
3
  import { memo, useState } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
 
6
- import { EditorCanvas } from '@/features/EditorCanvas';
6
+ import { useUserStore } from '@/store/user';
7
+ import { labPreferSelectors } from '@/store/user/selectors';
8
+
9
+ import EditorCanvas from './EditorCanvas';
10
+ import TextareCanvas from './TextArea';
7
11
 
8
12
  interface EditorModalProps extends ModalProps {
9
13
  onConfirm?: (value: string) => Promise<void>;
@@ -13,7 +17,8 @@ interface EditorModalProps extends ModalProps {
13
17
  export const EditorModal = memo<EditorModalProps>(({ value, onConfirm, ...rest }) => {
14
18
  const [confirmLoading, setConfirmLoading] = useState(false);
15
19
  const { t } = useTranslation('common');
16
-
20
+ const [v, setV] = useState(value);
21
+ const enableRichRender = useUserStore(labPreferSelectors.enableInputMarkdown);
17
22
  const editor = useEditor();
18
23
 
19
24
  return (
@@ -25,12 +30,13 @@ export const EditorModal = memo<EditorModalProps>(({ value, onConfirm, ...rest }
25
30
  okText={t('ok')}
26
31
  onOk={async () => {
27
32
  setConfirmLoading(true);
28
- try {
29
- await onConfirm?.((editor?.getDocument('markdown') as unknown as string) || '');
30
- } catch (e) {
31
- console.error('EditorModal onOk error:', e);
32
- onConfirm?.(value || '');
33
+ let finalValue;
34
+ if (enableRichRender) {
35
+ finalValue = editor?.getDocument('markdown') as unknown as string;
36
+ } else {
37
+ finalValue = v;
33
38
  }
39
+ await onConfirm?.(finalValue || '');
34
40
  setConfirmLoading(false);
35
41
  }}
36
42
  styles={{
@@ -43,7 +49,11 @@ export const EditorModal = memo<EditorModalProps>(({ value, onConfirm, ...rest }
43
49
  width={'min(90vw, 920px)'}
44
50
  {...rest}
45
51
  >
46
- <EditorCanvas editor={editor} editorData={{ content: value }} />
52
+ {enableRichRender ? (
53
+ <EditorCanvas defaultValue={value} editor={editor} />
54
+ ) : (
55
+ <TextareCanvas defaultValue={value} onChange={(v) => setV(v)} value={v} />
56
+ )}
47
57
  </Modal>
48
58
  );
49
59
  });
@@ -1,4 +1,4 @@
1
- import { ActionIcon, Block, Center, Text } from '@lobehub/ui';
1
+ import { Block, Center, Icon, Text } from '@lobehub/ui';
2
2
  import { PlusIcon } from 'lucide-react';
3
3
  import { memo } from 'react';
4
4
 
@@ -22,7 +22,7 @@ const EmptyNavItem = memo<EmptyStatusProps>(({ title, onClick, className }) => {
22
22
  variant={'borderless'}
23
23
  >
24
24
  <Center flex={'none'} height={28} width={28}>
25
- <ActionIcon icon={PlusIcon} size={'small'} />
25
+ <Icon icon={PlusIcon} size={'small'} />
26
26
  </Center>
27
27
  <Text align={'center'} type={'secondary'}>
28
28
  {title}
@@ -12,9 +12,10 @@ import {
12
12
  Text,
13
13
  } from '@lobehub/ui';
14
14
  import { createStaticStyles, cssVar, cx } from 'antd-style';
15
- import { Loader2Icon } from 'lucide-react';
16
15
  import { type ReactNode, memo } from 'react';
17
16
 
17
+ import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
18
+
18
19
  const ACTION_CLASS_NAME = 'nav-item-actions';
19
20
 
20
21
  const styles = createStaticStyles(({ css }) => ({
@@ -50,6 +51,10 @@ export interface NavItemProps extends Omit<BlockProps, 'children' | 'title'> {
50
51
  contextMenuItems?: GenericItemType[] | (() => GenericItemType[]);
51
52
  disabled?: boolean;
52
53
  extra?: ReactNode;
54
+ /**
55
+ * Optional href for cmd+click to open in new tab
56
+ */
57
+ href?: string;
53
58
  icon?: IconProps['icon'];
54
59
  loading?: boolean;
55
60
  title: ReactNode;
@@ -61,6 +66,7 @@ const NavItem = memo<NavItemProps>(
61
66
  actions,
62
67
  contextMenuItems,
63
68
  active,
69
+ href,
64
70
  icon,
65
71
  title,
66
72
  onClick,
@@ -72,7 +78,15 @@ const NavItem = memo<NavItemProps>(
72
78
  const iconColor = active ? cssVar.colorText : cssVar.colorTextDescription;
73
79
  const textColor = active ? cssVar.colorText : cssVar.colorTextSecondary;
74
80
  const variant = active ? 'filled' : 'borderless';
75
- const iconComponent = loading ? Loader2Icon : icon;
81
+
82
+ // Link props for cmd+click support
83
+ const linkProps = href
84
+ ? {
85
+ as: 'a' as const,
86
+ href,
87
+ style: { color: 'inherit', textDecoration: 'none' },
88
+ }
89
+ : {};
76
90
 
77
91
  const Content = (
78
92
  <Block
@@ -84,15 +98,25 @@ const NavItem = memo<NavItemProps>(
84
98
  horizontal
85
99
  onClick={(e) => {
86
100
  if (disabled || loading) return;
101
+ // Prevent default link behavior for normal clicks (let onClick handle it)
102
+ // But allow cmd+click to open in new tab
103
+ if (href && !e.metaKey && !e.ctrlKey) {
104
+ e.preventDefault();
105
+ }
87
106
  onClick?.(e);
88
107
  }}
89
108
  paddingInline={4}
90
109
  variant={variant}
110
+ {...linkProps}
91
111
  {...rest}
92
112
  >
93
113
  {icon && (
94
114
  <Center flex={'none'} height={28} width={28}>
95
- <Icon color={iconColor} icon={iconComponent} size={18} spin={loading} />
115
+ {loading ? (
116
+ <NeuralNetworkLoading size={18} />
117
+ ) : (
118
+ <Icon color={iconColor} icon={icon} size={18} />
119
+ )}
96
120
  </Center>
97
121
  )}
98
122
 
@@ -0,0 +1,167 @@
1
+ 'use client';
2
+
3
+ import { KLAVIS_SERVER_TYPES, type KlavisServerType } from '@lobechat/const';
4
+ import { Avatar, Icon, Tag } from '@lobehub/ui';
5
+ import { createStaticStyles, cssVar } from 'antd-style';
6
+ import isEqual from 'fast-deep-equal';
7
+ import { memo, useMemo } from 'react';
8
+
9
+ import PluginAvatar from '@/components/Plugins/PluginAvatar';
10
+ import { useIsDark } from '@/hooks/useIsDark';
11
+ import { useDiscoverStore } from '@/store/discover';
12
+ import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
13
+ import { useToolStore } from '@/store/tool';
14
+ import {
15
+ builtinToolSelectors,
16
+ klavisStoreSelectors,
17
+ pluginSelectors,
18
+ } from '@/store/tool/selectors';
19
+
20
+ /**
21
+ * Klavis server icon component
22
+ */
23
+ const KlavisIcon = memo<Pick<KlavisServerType, 'icon' | 'label'>>(({ icon, label }) => {
24
+ if (typeof icon === 'string') {
25
+ return <img alt={label} height={16} src={icon} style={{ flexShrink: 0 }} width={16} />;
26
+ }
27
+
28
+ return <Icon fill={cssVar.colorText} icon={icon} size={16} />;
29
+ });
30
+
31
+ const styles = createStaticStyles(({ css, cssVar }) => ({
32
+ compact: css`
33
+ height: auto !important;
34
+ padding: 0 !important;
35
+ border: none !important;
36
+ background: transparent !important;
37
+ `,
38
+ tag: css`
39
+ height: 24px !important;
40
+ border-radius: ${cssVar.borderRadiusSM} !important;
41
+ `,
42
+ }));
43
+
44
+ export interface ToolTagProps {
45
+ /**
46
+ * The tool identifier to display
47
+ */
48
+ identifier: string;
49
+ /**
50
+ * Variant style of the tag
51
+ * - 'default': normal tag with background and border
52
+ * - 'compact': no padding, no background, no border (text only with icon)
53
+ * @default 'default'
54
+ */
55
+ variant?: 'compact' | 'default';
56
+ }
57
+
58
+ /**
59
+ * A readonly tag component that displays tool information based on identifier.
60
+ * Unlike PluginTag, this component is not closable and is designed for display-only purposes.
61
+ */
62
+ const ToolTag = memo<ToolTagProps>(({ identifier, variant = 'default' }) => {
63
+ const isDarkMode = useIsDark();
64
+ const isCompact = variant === 'compact';
65
+
66
+ // Get local plugin lists
67
+ const builtinList = useToolStore(builtinToolSelectors.metaList, isEqual);
68
+ const installedPluginList = useToolStore(pluginSelectors.installedPluginMetaList, isEqual);
69
+
70
+ // Klavis related state
71
+ const allKlavisServers = useToolStore(klavisStoreSelectors.getServers, isEqual);
72
+ const isKlavisEnabledInEnv = useServerConfigStore(serverConfigSelectors.enableKlavis);
73
+
74
+ // Check if plugin is installed
75
+ const isInstalled = useToolStore(pluginSelectors.isPluginInstalled(identifier));
76
+
77
+ // Try to find in local lists first (including Klavis)
78
+ const localMeta = useMemo(() => {
79
+ // Check if it's a Klavis server type
80
+ if (isKlavisEnabledInEnv) {
81
+ const klavisType = KLAVIS_SERVER_TYPES.find((type) => type.identifier === identifier);
82
+ if (klavisType) {
83
+ const connectedServer = allKlavisServers.find((s) => s.identifier === identifier);
84
+ return {
85
+ icon: klavisType.icon,
86
+ isInstalled: !!connectedServer,
87
+ label: klavisType.label,
88
+ title: klavisType.label,
89
+ type: 'klavis' as const,
90
+ };
91
+ }
92
+ }
93
+
94
+ const builtinMeta = builtinList.find((p) => p.identifier === identifier);
95
+ if (builtinMeta) {
96
+ return {
97
+ avatar: builtinMeta.meta.avatar,
98
+ isInstalled: true,
99
+ title: builtinMeta.meta.title,
100
+ type: 'builtin' as const,
101
+ };
102
+ }
103
+
104
+ const installedMeta = installedPluginList.find((p) => p.identifier === identifier);
105
+ if (installedMeta) {
106
+ return {
107
+ avatar: installedMeta.avatar,
108
+ isInstalled: true,
109
+ title: installedMeta.title,
110
+ type: 'plugin' as const,
111
+ };
112
+ }
113
+
114
+ return null;
115
+ }, [identifier, builtinList, installedPluginList, isKlavisEnabledInEnv, allKlavisServers]);
116
+
117
+ // Fetch from remote if not found locally
118
+ const usePluginDetail = useDiscoverStore((s) => s.usePluginDetail);
119
+ const { data: remoteData, isLoading } = usePluginDetail({
120
+ identifier: !localMeta && !isInstalled ? identifier : undefined,
121
+ withManifest: false,
122
+ });
123
+
124
+ // Determine final metadata
125
+ const meta = localMeta || {
126
+ avatar: remoteData?.avatar,
127
+ isInstalled: false,
128
+ title: remoteData?.title || identifier,
129
+ type: 'plugin' as const,
130
+ };
131
+
132
+ const displayTitle = isLoading ? 'Loading...' : meta.title;
133
+
134
+ // Render icon based on type
135
+ const renderIcon = () => {
136
+ // Klavis type has icon property
137
+ if (meta.type === 'klavis' && 'icon' in meta && 'label' in meta) {
138
+ return <KlavisIcon icon={meta.icon} label={meta.label} />;
139
+ }
140
+
141
+ // Builtin type has avatar
142
+ if (meta.type === 'builtin' && 'avatar' in meta && meta.avatar) {
143
+ return <Avatar avatar={meta.avatar} shape={'square'} size={16} style={{ flexShrink: 0 }} />;
144
+ }
145
+
146
+ // Plugin type
147
+ if ('avatar' in meta) {
148
+ return <PluginAvatar avatar={meta.avatar} size={16} />;
149
+ }
150
+
151
+ return null;
152
+ };
153
+
154
+ return (
155
+ <Tag
156
+ className={isCompact ? styles.compact : styles.tag}
157
+ icon={renderIcon()}
158
+ variant={isCompact ? 'borderless' : isDarkMode ? 'filled' : 'outlined'}
159
+ >
160
+ {displayTitle}
161
+ </Tag>
162
+ );
163
+ });
164
+
165
+ ToolTag.displayName = 'ToolTag';
166
+
167
+ export default ToolTag;
@@ -3,6 +3,7 @@ import {
3
3
  type RecentTopicGroup,
4
4
  type RecentTopicGroupMember,
5
5
  } from '@lobechat/types';
6
+ import { cleanObject } from '@lobechat/utils';
6
7
  import { eq, inArray } from 'drizzle-orm';
7
8
  import { after } from 'next/server';
8
9
  import { z } from 'zod';
@@ -410,8 +411,14 @@ export const topicRouter = router({
410
411
  const agentId = topicAgentIdMap.get(topic.id);
411
412
  const agentInfo = agentId ? agentInfoMap.get(agentId) : null;
412
413
 
414
+ // Clean agent info - if avatar/title are all null, return null
415
+ const cleanedAgent = agentInfo ? cleanObject(agentInfo) : null;
416
+ // Only return agent if it has meaningful display info (avatar or title)
417
+ const validAgent =
418
+ cleanedAgent && (cleanedAgent.avatar || cleanedAgent.title) ? cleanedAgent : null;
419
+
413
420
  return {
414
- agent: agentInfo ?? null,
421
+ agent: validAgent,
415
422
  group: null,
416
423
  id: topic.id,
417
424
  title: topic.title,
@@ -448,7 +448,7 @@ describe('contextEngineering', () => {
448
448
  ];
449
449
 
450
450
  // Mock topic memories and global identities separately
451
- vi.spyOn(memoryManager, 'resolveTopicMemories').mockResolvedValue({
451
+ vi.spyOn(memoryManager, 'resolveTopicMemories').mockReturnValue({
452
452
  contexts: [
453
453
  {
454
454
  accessedAt: new Date('2024-01-01T00:00:00.000Z'),
@@ -252,12 +252,11 @@ export const contextEngineering = async ({
252
252
  .map((kb) => ({ description: kb.description, id: kb.id, name: kb.name }));
253
253
 
254
254
  // Resolve user memories: topic memories and global identities are independent layers
255
+ // Both functions now read from cache only (no network requests) to avoid blocking sendMessage
255
256
  let userMemoryData;
256
257
  if (enableUserMemories) {
257
- const [topicMemories, globalIdentities] = await Promise.all([
258
- resolveTopicMemories(),
259
- Promise.resolve(resolveGlobalIdentities()),
260
- ]);
258
+ const topicMemories = resolveTopicMemories();
259
+ const globalIdentities = resolveGlobalIdentities();
261
260
  userMemoryData = combineUserMemoryData(topicMemories, globalIdentities);
262
261
  }
263
262
 
@@ -1,10 +1,8 @@
1
1
  import type { UserMemoryData, UserMemoryIdentityItem } from '@lobechat/context-engine';
2
2
  import type { RetrieveMemoryResult } from '@lobechat/types';
3
3
 
4
- import { mutate } from '@/libs/swr';
5
- import { userMemoryService } from '@/services/userMemory';
6
4
  import { getChatStoreState } from '@/store/chat';
7
- import { getUserMemoryStoreState, useUserMemoryStore } from '@/store/userMemory';
5
+ import { getUserMemoryStoreState } from '@/store/userMemory';
8
6
  import { agentMemorySelectors, identitySelectors } from '@/store/userMemory/selectors';
9
7
 
10
8
  const EMPTY_MEMORIES: RetrieveMemoryResult = {
@@ -39,17 +37,13 @@ export interface TopicMemoryResolverContext {
39
37
  }
40
38
 
41
39
  /**
42
- * Resolves topic-based memories (contexts, experiences, preferences)
40
+ * Resolves topic-based memories (contexts, experiences, preferences) from cache only.
43
41
  *
44
- * This function handles:
45
- * 1. Getting the topic ID from context or active topic
46
- * 2. Checking if memories are already cached for the topic
47
- * 3. Fetching memories from the service if not cached
48
- * 4. Caching the fetched memories by topic ID
42
+ * This function only reads from cache and does NOT trigger network requests.
43
+ * Memory data is pre-loaded by SWR in ChatList via useFetchTopicMemories hook.
44
+ * This ensures sendMessage is not blocked by memory retrieval network calls.
49
45
  */
50
- export const resolveTopicMemories = async (
51
- ctx?: TopicMemoryResolverContext,
52
- ): Promise<RetrieveMemoryResult> => {
46
+ export const resolveTopicMemories = (ctx?: TopicMemoryResolverContext): RetrieveMemoryResult => {
53
47
  // Get topic ID from context or active topic
54
48
  const topicId = ctx?.topicId ?? getChatStoreState().activeTopicId;
55
49
 
@@ -60,34 +54,11 @@ export const resolveTopicMemories = async (
60
54
 
61
55
  const userMemoryStoreState = getUserMemoryStoreState();
62
56
 
63
- // Check if already have cached memories for this topic
57
+ // Only read from cache, do not trigger network request
58
+ // Memory data is pre-loaded by SWR in ChatList
64
59
  const cachedMemories = agentMemorySelectors.topicMemories(topicId)(userMemoryStoreState);
65
60
 
66
- if (cachedMemories) {
67
- return cachedMemories;
68
- }
69
-
70
- // Fetch memories for this topic
71
- try {
72
- const result = await userMemoryService.retrieveMemoryForTopic(topicId);
73
- const memories = result ?? EMPTY_MEMORIES;
74
-
75
- // Cache the fetched memories by topic ID
76
- useUserMemoryStore.setState((state) => ({
77
- topicMemoriesMap: {
78
- ...state.topicMemoriesMap,
79
- [topicId]: memories,
80
- },
81
- }));
82
-
83
- // Also trigger SWR mutate to keep in sync
84
- await mutate(['useFetchMemoriesForTopic', topicId]);
85
-
86
- return memories;
87
- } catch (error) {
88
- console.error('Failed to retrieve memories for topic:', error);
89
- return EMPTY_MEMORIES;
90
- }
61
+ return cachedMemories ?? EMPTY_MEMORIES;
91
62
  };
92
63
 
93
64
  /**
@@ -4,7 +4,7 @@ import type { ParsedQuery } from 'query-string';
4
4
  import type { ChatGroupItem } from '@/database/schemas/chatGroup';
5
5
 
6
6
  export interface QueryRouter {
7
- push: (url: string, options?: { query?: ParsedQuery }) => void;
7
+ push: (url: string, options?: { query?: ParsedQuery; replace?: boolean }) => void;
8
8
  }
9
9
 
10
10
  export interface ChatGroupState {