@lobehub/lobehub 2.0.0-next.285 → 2.0.0-next.287

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 (24) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/apps/desktop/src/main/const/theme.ts +0 -3
  3. package/apps/desktop/src/main/core/browser/Browser.ts +1 -1
  4. package/apps/desktop/src/main/core/browser/WindowThemeManager.ts +3 -2
  5. package/apps/desktop/src/main/core/browser/__tests__/Browser.test.ts +0 -1
  6. package/apps/desktop/src/main/core/browser/__tests__/WindowThemeManager.test.ts +8 -5
  7. package/changelog/v1.json +14 -0
  8. package/package.json +1 -1
  9. package/packages/business/const/src/index.ts +0 -3
  10. package/packages/desktop-bridge/src/index.ts +3 -0
  11. package/src/app/[variants]/(main)/agent/features/Conversation/AgentWelcome/AddButton.tsx +2 -1
  12. package/src/app/[variants]/(main)/agent/features/Conversation/Header/ShareButton/index.tsx +5 -3
  13. package/src/app/[variants]/(main)/community/(detail)/provider/features/Details/Nav.tsx +27 -18
  14. package/src/app/[variants]/(main)/group/features/Conversation/Header/ShareButton/index.tsx +5 -3
  15. package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +15 -2
  16. package/src/app/[variants]/onboarding/_layout/style.ts +10 -20
  17. package/src/features/ElectronTitlebar/Connection/ConnectionMode.tsx +2 -2
  18. package/src/features/ElectronTitlebar/SimpleTitleBar.tsx +1 -2
  19. package/src/features/ElectronTitlebar/index.tsx +2 -2
  20. package/src/hooks/useUserAvatar.test.ts +23 -4
  21. package/src/store/electron/selectors/sync.ts +17 -1
  22. package/src/store/user/slices/settings/action.test.ts +25 -0
  23. package/src/store/user/slices/settings/action.ts +11 -0
  24. package/src/features/ElectronTitlebar/const.ts +0 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,64 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.287](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.286...v2.0.0-next.287)
6
+
7
+ <sup>Released on **2026-01-14**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **desktop**: Unify TITLE_BAR_HEIGHT constant to desktop-bridge.
12
+
13
+ #### 🐛 Bug Fixes
14
+
15
+ - **desktop**: Return OFFICIAL_URL in cloud mode for remoteServerUrl selector.
16
+
17
+ <br/>
18
+
19
+ <details>
20
+ <summary><kbd>Improvements and Fixes</kbd></summary>
21
+
22
+ #### Code refactoring
23
+
24
+ - **desktop**: Unify TITLE_BAR_HEIGHT constant to desktop-bridge, closes [#11496](https://github.com/lobehub/lobe-chat/issues/11496) ([e7739e5](https://github.com/lobehub/lobe-chat/commit/e7739e5))
25
+
26
+ #### What's fixed
27
+
28
+ - **desktop**: Return OFFICIAL_URL in cloud mode for remoteServerUrl selector, closes [#11502](https://github.com/lobehub/lobe-chat/issues/11502) ([1d11fac](https://github.com/lobehub/lobe-chat/commit/1d11fac))
29
+
30
+ </details>
31
+
32
+ <div align="right">
33
+
34
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
35
+
36
+ </div>
37
+
38
+ ## [Version 2.0.0-next.286](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.285...v2.0.0-next.286)
39
+
40
+ <sup>Released on **2026-01-14**</sup>
41
+
42
+ #### 🐛 Bug Fixes
43
+
44
+ - **misc**: Prevent auto navigation to profile when clicking topic.
45
+
46
+ <br/>
47
+
48
+ <details>
49
+ <summary><kbd>Improvements and Fixes</kbd></summary>
50
+
51
+ #### What's fixed
52
+
53
+ - **misc**: Prevent auto navigation to profile when clicking topic, closes [#11500](https://github.com/lobehub/lobe-chat/issues/11500) ([1e03005](https://github.com/lobehub/lobe-chat/commit/1e03005))
54
+
55
+ </details>
56
+
57
+ <div align="right">
58
+
59
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
60
+
61
+ </div>
62
+
5
63
  ## [Version 2.0.0-next.285](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.284...v2.0.0-next.285)
6
64
 
7
65
  <sup>Released on **2026-01-14**</sup>
@@ -4,8 +4,5 @@ export const BACKGROUND_LIGHT = '#f8f8f8';
4
4
  export const SYMBOL_COLOR_DARK = '#ffffff80';
5
5
  export const SYMBOL_COLOR_LIGHT = '#00000080';
6
6
 
7
- // Window dimensions and constraints
8
- export const TITLE_BAR_HEIGHT = 29;
9
-
10
7
  // Default window configuration
11
8
  export const THEME_CHANGE_DELAY = 100;
@@ -1,3 +1,4 @@
1
+ import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
1
2
  import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
2
3
  import {
3
4
  BrowserWindow,
@@ -12,7 +13,6 @@ import { join } from 'node:path';
12
13
  import { preloadDir, resourcesDir } from '@/const/dir';
13
14
  import { isMac } from '@/const/env';
14
15
  import { ELECTRON_BE_PROTOCOL_SCHEME } from '@/const/protocol';
15
- import { TITLE_BAR_HEIGHT } from '@/const/theme';
16
16
  import RemoteServerConfigCtr from '@/controllers/RemoteServerConfigCtr';
17
17
  import { backendProxyProtocolManager } from '@/core/infrastructure/BackendProxyProtocolManager';
18
18
  import { setResponseHeader } from '@/utils/http-headers';
@@ -1,3 +1,4 @@
1
+ import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
1
2
  import { BrowserWindow, nativeTheme } from 'electron';
2
3
  import { join } from 'node:path';
3
4
 
@@ -9,7 +10,6 @@ import {
9
10
  SYMBOL_COLOR_DARK,
10
11
  SYMBOL_COLOR_LIGHT,
11
12
  THEME_CHANGE_DELAY,
12
- TITLE_BAR_HEIGHT,
13
13
  } from '@/const/theme';
14
14
  import { createLogger } from '@/utils/logger';
15
15
 
@@ -91,7 +91,8 @@ export class WindowThemeManager {
91
91
  icon: isDev ? join(buildDir, 'icon-dev.ico') : undefined,
92
92
  titleBarOverlay: {
93
93
  color: isDarkMode ? BACKGROUND_DARK : BACKGROUND_LIGHT,
94
- height: TITLE_BAR_HEIGHT,
94
+ // Reduce 2px to prevent blocking the container border edge
95
+ height: TITLE_BAR_HEIGHT - 2,
95
96
  symbolColor: isDarkMode ? SYMBOL_COLOR_DARK : SYMBOL_COLOR_LIGHT,
96
97
  },
97
98
  titleBarStyle: 'hidden',
@@ -108,7 +108,6 @@ vi.mock('@/const/theme', () => ({
108
108
  SYMBOL_COLOR_DARK: '#ffffff',
109
109
  SYMBOL_COLOR_LIGHT: '#000000',
110
110
  THEME_CHANGE_DELAY: 0,
111
- TITLE_BAR_HEIGHT: 32,
112
111
  }));
113
112
 
114
113
  describe('Browser', () => {
@@ -38,13 +38,16 @@ vi.mock('@/const/env', () => ({
38
38
  isWindows: true,
39
39
  }));
40
40
 
41
+ vi.mock('@lobechat/desktop-bridge', () => ({
42
+ TITLE_BAR_HEIGHT: 38,
43
+ }));
44
+
41
45
  vi.mock('@/const/theme', () => ({
42
46
  BACKGROUND_DARK: '#1a1a1a',
43
47
  BACKGROUND_LIGHT: '#ffffff',
44
48
  SYMBOL_COLOR_DARK: '#ffffff',
45
49
  SYMBOL_COLOR_LIGHT: '#000000',
46
50
  THEME_CHANGE_DELAY: 0,
47
- TITLE_BAR_HEIGHT: 32,
48
51
  }));
49
52
 
50
53
  describe('WindowThemeManager', () => {
@@ -89,7 +92,7 @@ describe('WindowThemeManager', () => {
89
92
  icon: undefined,
90
93
  titleBarOverlay: {
91
94
  color: '#1a1a1a',
92
- height: 32,
95
+ height: 36,
93
96
  symbolColor: '#ffffff',
94
97
  },
95
98
  titleBarStyle: 'hidden',
@@ -106,7 +109,7 @@ describe('WindowThemeManager', () => {
106
109
  icon: undefined,
107
110
  titleBarOverlay: {
108
111
  color: '#ffffff',
109
- height: 32,
112
+ height: 36,
110
113
  symbolColor: '#000000',
111
114
  },
112
115
  titleBarStyle: 'hidden',
@@ -183,7 +186,7 @@ describe('WindowThemeManager', () => {
183
186
  expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalledWith('#1a1a1a');
184
187
  expect(mockBrowserWindow.setTitleBarOverlay).toHaveBeenCalledWith({
185
188
  color: '#1a1a1a',
186
- height: 32,
189
+ height: 36,
187
190
  symbolColor: '#ffffff',
188
191
  });
189
192
  });
@@ -195,7 +198,7 @@ describe('WindowThemeManager', () => {
195
198
  expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalledWith('#ffffff');
196
199
  expect(mockBrowserWindow.setTitleBarOverlay).toHaveBeenCalledWith({
197
200
  color: '#ffffff',
198
- height: 32,
201
+ height: 36,
199
202
  symbolColor: '#000000',
200
203
  });
201
204
  });
package/changelog/v1.json CHANGED
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "children": {},
4
+ "date": "2026-01-14",
5
+ "version": "2.0.0-next.287"
6
+ },
7
+ {
8
+ "children": {
9
+ "fixes": [
10
+ "Prevent auto navigation to profile when clicking topic."
11
+ ]
12
+ },
13
+ "date": "2026-01-14",
14
+ "version": "2.0.0-next.286"
15
+ },
2
16
  {
3
17
  "children": {
4
18
  "features": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.285",
3
+ "version": "2.0.0-next.287",
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",
@@ -4,6 +4,3 @@ export * from './llm';
4
4
  export * from './url';
5
5
 
6
6
  export const ENABLE_BUSINESS_FEATURES = false;
7
- export const ENABLE_TOPIC_LINK_SHARE =
8
- ENABLE_BUSINESS_FEATURES ||
9
- (process.env.NODE_ENV === 'development' && !!process.env.NEXT_PUBLIC_ENABLE_TOPIC_LINK_SHARE);
@@ -7,3 +7,6 @@ export {
7
7
  locales,
8
8
  RouteVariants,
9
9
  } from './routeVariants';
10
+
11
+ // Desktop window constants
12
+ export const TITLE_BAR_HEIGHT = 38;
@@ -9,7 +9,8 @@ import { useAgentStore } from '@/store/agent';
9
9
  const AddButton = memo(() => {
10
10
  const navigate = useNavigate();
11
11
  const createAgent = useAgentStore((s) => s.createAgent);
12
- const { mutate, isValidating } = useActionSWR('agent.createAgent', async () => {
12
+ // Use a unique SWR key to avoid conflicts with useCreateMenuItems which uses 'agent.createAgent'
13
+ const { mutate, isValidating } = useActionSWR('agent.createAgentFromWelcome', async () => {
13
14
  const result = await createAgent({});
14
15
  navigate(`/agent/${result.agentId}/profile`);
15
16
  return result;
@@ -1,6 +1,5 @@
1
1
  'use client';
2
2
 
3
- import { ENABLE_TOPIC_LINK_SHARE } from '@lobechat/business-const';
4
3
  import { ActionIcon } from '@lobehub/ui';
5
4
  import { Share2 } from 'lucide-react';
6
5
  import dynamic from 'next/dynamic';
@@ -10,6 +9,8 @@ import { useTranslation } from 'react-i18next';
10
9
  import { DESKTOP_HEADER_ICON_SIZE, MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens';
11
10
  import { useWorkspaceModal } from '@/hooks/useWorkspaceModal';
12
11
  import { useChatStore } from '@/store/chat';
12
+ import { useServerConfigStore } from '@/store/serverConfig';
13
+ import { serverConfigSelectors } from '@/store/serverConfig/selectors';
13
14
 
14
15
  const ShareModal = dynamic(() => import('@/features/ShareModal'));
15
16
  const SharePopover = dynamic(() => import('@/features/SharePopover'));
@@ -24,6 +25,7 @@ const ShareButton = memo<ShareButtonProps>(({ mobile, setOpen, open }) => {
24
25
  const [isModalOpen, setIsModalOpen] = useWorkspaceModal(open, setOpen);
25
26
  const { t } = useTranslation('common');
26
27
  const activeTopicId = useChatStore((s) => s.activeTopicId);
28
+ const enableTopicLinkShare = useServerConfigStore(serverConfigSelectors.enableBusinessFeatures);
27
29
 
28
30
  // Hide share button when no topic exists (no messages sent yet)
29
31
  if (!activeTopicId) return null;
@@ -31,7 +33,7 @@ const ShareButton = memo<ShareButtonProps>(({ mobile, setOpen, open }) => {
31
33
  const iconButton = (
32
34
  <ActionIcon
33
35
  icon={Share2}
34
- onClick={ENABLE_TOPIC_LINK_SHARE ? undefined : () => setIsModalOpen(true)}
36
+ onClick={enableTopicLinkShare ? undefined : () => setIsModalOpen(true)}
35
37
  size={mobile ? MOBILE_HEADER_ICON_SIZE : DESKTOP_HEADER_ICON_SIZE}
36
38
  title={t('share')}
37
39
  tooltipProps={{
@@ -42,7 +44,7 @@ const ShareButton = memo<ShareButtonProps>(({ mobile, setOpen, open }) => {
42
44
 
43
45
  return (
44
46
  <>
45
- {ENABLE_TOPIC_LINK_SHARE ? (
47
+ {enableTopicLinkShare ? (
46
48
  <SharePopover onOpenModal={() => setIsModalOpen(true)}>{iconButton}</SharePopover>
47
49
  ) : (
48
50
  iconButton
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { SOCIAL_URL } from '@lobechat/business-const';
3
+ import { BRANDING_PROVIDER, SOCIAL_URL } from '@lobechat/business-const';
4
4
  import { Flexbox, Icon, Tabs } from '@lobehub/ui';
5
5
  import { createStaticStyles } from 'antd-style';
6
6
  import { BookOpenIcon, BrainCircuitIcon, ListIcon } from 'lucide-react';
@@ -38,27 +38,36 @@ const Nav = memo<NavProps>(({ mobile, setActiveTab, activeTab = ProviderNavKey.O
38
38
  const { t } = useTranslation('discover');
39
39
  const { identifier } = useDetailContext();
40
40
 
41
+ // Hide Guide tab for branding provider as it doesn't have integration docs
42
+ const showGuideTab = identifier !== BRANDING_PROVIDER;
43
+
44
+ const items = [
45
+ {
46
+ icon: <Icon icon={BookOpenIcon} size={16} />,
47
+ key: ProviderNavKey.Overview,
48
+ label: t('providers.details.overview.title'),
49
+ },
50
+ ...(showGuideTab
51
+ ? [
52
+ {
53
+ icon: <Icon icon={BrainCircuitIcon} size={16} />,
54
+ key: ProviderNavKey.Guide,
55
+ label: t('providers.details.guide.title'),
56
+ },
57
+ ]
58
+ : []),
59
+ {
60
+ icon: <Icon icon={ListIcon} size={16} />,
61
+ key: ProviderNavKey.Related,
62
+ label: t('providers.details.related.title'),
63
+ },
64
+ ];
65
+
41
66
  const nav = (
42
67
  <Tabs
43
68
  activeKey={activeTab}
44
69
  compact={mobile}
45
- items={[
46
- {
47
- icon: <Icon icon={BookOpenIcon} size={16} />,
48
- key: ProviderNavKey.Overview,
49
- label: t('providers.details.overview.title'),
50
- },
51
- {
52
- icon: <Icon icon={BrainCircuitIcon} size={16} />,
53
- key: ProviderNavKey.Guide,
54
- label: t('providers.details.guide.title'),
55
- },
56
- {
57
- icon: <Icon icon={ListIcon} size={16} />,
58
- key: ProviderNavKey.Related,
59
- label: t('providers.details.related.title'),
60
- },
61
- ]}
70
+ items={items}
62
71
  onChange={(key) => setActiveTab?.(key as ProviderNavKey)}
63
72
  />
64
73
  );
@@ -1,6 +1,5 @@
1
1
  'use client';
2
2
 
3
- import { ENABLE_TOPIC_LINK_SHARE } from '@lobechat/business-const';
4
3
  import { ActionIcon } from '@lobehub/ui';
5
4
  import { Share2 } from 'lucide-react';
6
5
  import dynamic from 'next/dynamic';
@@ -10,6 +9,8 @@ import { useTranslation } from 'react-i18next';
10
9
  import { DESKTOP_HEADER_ICON_SIZE, MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens';
11
10
  import { useWorkspaceModal } from '@/hooks/useWorkspaceModal';
12
11
  import { useChatStore } from '@/store/chat';
12
+ import { useServerConfigStore } from '@/store/serverConfig';
13
+ import { serverConfigSelectors } from '@/store/serverConfig/selectors';
13
14
 
14
15
  const ShareModal = dynamic(() => import('@/features/ShareModal'));
15
16
  const SharePopover = dynamic(() => import('@/features/SharePopover'));
@@ -24,6 +25,7 @@ const ShareButton = memo<ShareButtonProps>(({ mobile, setOpen, open }) => {
24
25
  const [isModalOpen, setIsModalOpen] = useWorkspaceModal(open, setOpen);
25
26
  const { t } = useTranslation('common');
26
27
  const activeTopicId = useChatStore((s) => s.activeTopicId);
28
+ const enableTopicLinkShare = useServerConfigStore(serverConfigSelectors.enableBusinessFeatures);
27
29
 
28
30
  // Hide share button when no topic exists (no messages sent yet)
29
31
  if (!activeTopicId) return null;
@@ -31,7 +33,7 @@ const ShareButton = memo<ShareButtonProps>(({ mobile, setOpen, open }) => {
31
33
  const iconButton = (
32
34
  <ActionIcon
33
35
  icon={Share2}
34
- onClick={ENABLE_TOPIC_LINK_SHARE ? undefined : () => setIsModalOpen(true)}
36
+ onClick={enableTopicLinkShare ? undefined : () => setIsModalOpen(true)}
35
37
  size={mobile ? MOBILE_HEADER_ICON_SIZE : DESKTOP_HEADER_ICON_SIZE}
36
38
  title={t('share')}
37
39
  tooltipProps={{
@@ -42,7 +44,7 @@ const ShareButton = memo<ShareButtonProps>(({ mobile, setOpen, open }) => {
42
44
 
43
45
  return (
44
46
  <>
45
- {ENABLE_TOPIC_LINK_SHARE ? (
47
+ {enableTopicLinkShare ? (
46
48
  <SharePopover onOpenModal={() => setIsModalOpen(true)}>{iconButton}</SharePopover>
47
49
  ) : (
48
50
  iconButton
@@ -26,6 +26,8 @@ import {
26
26
  import { useMemo } from 'react';
27
27
  import { useTranslation } from 'react-i18next';
28
28
 
29
+ import { useElectronStore } from '@/store/electron';
30
+ import { electronSyncSelectors } from '@/store/electron/selectors';
29
31
  import { SettingsTabs } from '@/store/global/initialState';
30
32
  import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
31
33
  import { useUserStore } from '@/store/user';
@@ -63,13 +65,24 @@ export const useCategory = () => {
63
65
  userProfileSelectors.userAvatar(s),
64
66
  userProfileSelectors.nickName(s),
65
67
  ]);
68
+ const remoteServerUrl = useElectronStore(electronSyncSelectors.remoteServerUrl);
69
+
70
+ // Process avatar URL for desktop environment
71
+ const avatarUrl = useMemo(() => {
72
+ if (!avatar) return undefined;
73
+ if (isDesktop && avatar.startsWith('/') && remoteServerUrl) {
74
+ return remoteServerUrl + avatar;
75
+ }
76
+ return avatar;
77
+ }, [avatar, remoteServerUrl]);
78
+
66
79
  const categoryGroups: CategoryGroup[] = useMemo(() => {
67
80
  const groups: CategoryGroup[] = [];
68
81
 
69
82
  // 个人资料组 - Profile 相关设置
70
83
  const profileItems: CategoryItem[] = [
71
84
  {
72
- icon: avatar ? <Avatar avatar={avatar} shape={'square'} size={26} /> : UserCircle,
85
+ icon: avatarUrl ? <Avatar avatar={avatarUrl} shape={'square'} size={26} /> : UserCircle,
73
86
  key: SettingsTabs.Profile,
74
87
  label: username ? username : tAuth('tab.profile'),
75
88
  },
@@ -227,7 +240,7 @@ export const useCategory = () => {
227
240
  showAiImage,
228
241
  showApiKeyManage,
229
242
  isLoginWithClerk,
230
- avatar,
243
+ avatarUrl,
231
244
  username,
232
245
  ]);
233
246
 
@@ -1,21 +1,16 @@
1
1
  import { createStaticStyles } from 'antd-style';
2
2
 
3
3
  export const styles = createStaticStyles(({ css, cssVar }) => ({
4
-
5
-
6
- // Divider 样式
7
- divider: css`
4
+ // Divider 样式
5
+ divider: css`
8
6
  height: 24px;
9
7
  `,
10
8
 
11
-
12
-
13
-
14
- // 内层容器 - 深色模式
15
- innerContainerDark: css`
9
+ // 内层容器 - 深色模式
10
+ innerContainerDark: css`
16
11
  position: relative;
17
12
 
18
- overflow: hidden;
13
+ overflow: hidden auto;
19
14
 
20
15
  border: 1px solid ${cssVar.colorBorderSecondary};
21
16
  border-radius: ${cssVar.borderRadius};
@@ -23,14 +18,11 @@ innerContainerDark: css`
23
18
  background: ${cssVar.colorBgContainer};
24
19
  `,
25
20
 
26
-
27
-
28
-
29
- // 内层容器 - 浅色模式
30
- innerContainerLight: css`
21
+ // 内层容器 - 浅色模式
22
+ innerContainerLight: css`
31
23
  position: relative;
32
24
 
33
- overflow: hidden;
25
+ overflow: hidden auto;
34
26
 
35
27
  border: 1px solid ${cssVar.colorBorder};
36
28
  border-radius: ${cssVar.borderRadius};
@@ -38,10 +30,8 @@ innerContainerLight: css`
38
30
  background: ${cssVar.colorBgContainer};
39
31
  `,
40
32
 
41
-
42
-
43
- // 外层容器
44
- outerContainer: css`
33
+ // 外层容器
34
+ outerContainer: css`
45
35
  position: relative;
46
36
  `,
47
37
  }));
@@ -82,12 +82,12 @@ const ConnectionMode = memo<ConnectionModeProps>(({ setWaiting }) => {
82
82
 
83
83
  const connect = useElectronStore((s) => s.connectRemoteServer);
84
84
  const storageMode = useElectronStore(electronSyncSelectors.storageMode);
85
- const remoteServerUrl = useElectronStore(electronSyncSelectors.remoteServerUrl);
85
+ const rawRemoteServerUrl = useElectronStore(electronSyncSelectors.rawRemoteServerUrl);
86
86
 
87
87
  const [selectedOption, setSelectedOption] = useState<RemoteStorageMode>(
88
88
  storageMode === StorageModeEnum.SelfHost ? StorageModeEnum.SelfHost : StorageModeEnum.Cloud,
89
89
  );
90
- const [selfHostedUrl, setSelfHostedUrl] = useState(remoteServerUrl);
90
+ const [selfHostedUrl, setSelfHostedUrl] = useState(rawRemoteServerUrl);
91
91
 
92
92
  const validateUrl = useCallback((url: string) => {
93
93
  if (!url) {
@@ -1,13 +1,12 @@
1
1
  'use client';
2
2
 
3
+ import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
3
4
  import { Flexbox } from '@lobehub/ui';
4
5
  import { type FC } from 'react';
5
6
 
6
7
  import { ProductLogo } from '@/components/Branding/ProductLogo';
7
8
  import { electronStylish } from '@/styles/electron';
8
9
 
9
- import { TITLE_BAR_HEIGHT } from './const';
10
-
11
10
  /**
12
11
  * A simple, minimal TitleBar for Electron windows.
13
12
  * Provides draggable area without business logic (navigation, updates, etc.)
@@ -1,3 +1,4 @@
1
+ import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
1
2
  import { Flexbox } from '@lobehub/ui';
2
3
  import { Divider } from 'antd';
3
4
  import { memo, useMemo } from 'react';
@@ -11,7 +12,6 @@ import NavigationBar from './NavigationBar';
11
12
  import { UpdateModal } from './UpdateModal';
12
13
  import { UpdateNotification } from './UpdateNotification';
13
14
  import WinControl from './WinControl';
14
- import { TITLE_BAR_HEIGHT } from './const';
15
15
  import { useWatchThemeUpdate } from './hooks/useWatchThemeUpdate';
16
16
 
17
17
  const isMac = isMacOS();
@@ -66,5 +66,5 @@ const TitleBar = memo(() => {
66
66
 
67
67
  export default TitleBar;
68
68
 
69
- export { TITLE_BAR_HEIGHT } from './const';
70
69
  export { default as SimpleTitleBar } from './SimpleTitleBar';
70
+ export { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
@@ -19,6 +19,7 @@ vi.mock('@lobechat/const', async (importOriginal) => {
19
19
  return mockIsDesktop;
20
20
  },
21
21
  DEFAULT_USER_AVATAR: 'default-avatar.png',
22
+ OFFICIAL_URL: 'https://app.lobehub.com',
22
23
  };
23
24
  });
24
25
 
@@ -77,7 +78,7 @@ describe('useUserAvatar', () => {
77
78
  expect(result.current).toBe(mockAvatar);
78
79
  });
79
80
 
80
- it('should prepend remote server URL when avatar starts with / in desktop environment', () => {
81
+ it('should prepend remote server URL when avatar starts with / in desktop environment (selfHost mode)', () => {
81
82
  mockIsDesktop = true;
82
83
  const mockAvatar = '/api/avatar.png';
83
84
  const mockServerUrl = 'https://server.com';
@@ -85,7 +86,7 @@ describe('useUserAvatar', () => {
85
86
  act(() => {
86
87
  useUserStore.setState({ user: { avatar: mockAvatar } as any });
87
88
  useElectronStore.setState({
88
- dataSyncConfig: { remoteServerUrl: mockServerUrl, storageMode: 'cloud' },
89
+ dataSyncConfig: { remoteServerUrl: mockServerUrl, storageMode: 'selfHost' },
89
90
  });
90
91
  });
91
92
 
@@ -102,7 +103,7 @@ describe('useUserAvatar', () => {
102
103
  act(() => {
103
104
  useUserStore.setState({ user: { avatar: mockAvatar } as any });
104
105
  useElectronStore.setState({
105
- dataSyncConfig: { remoteServerUrl: mockServerUrl, storageMode: 'cloud' },
106
+ dataSyncConfig: { remoteServerUrl: mockServerUrl, storageMode: 'selfHost' },
106
107
  });
107
108
  });
108
109
 
@@ -111,7 +112,7 @@ describe('useUserAvatar', () => {
111
112
  expect(result.current).toBe(mockAvatar);
112
113
  });
113
114
 
114
- it('should handle empty remote server URL in desktop environment', () => {
115
+ it('should use OFFICIAL_URL when storageMode is cloud in desktop environment', () => {
115
116
  mockIsDesktop = true;
116
117
  const mockAvatar = '/api/avatar.png';
117
118
 
@@ -124,6 +125,24 @@ describe('useUserAvatar', () => {
124
125
 
125
126
  const { result } = renderHook(() => useUserAvatar());
126
127
 
128
+ // In cloud mode, selector returns OFFICIAL_URL regardless of remoteServerUrl config
129
+ expect(result.current).toBe('https://app.lobehub.com/api/avatar.png');
130
+ });
131
+
132
+ it('should return original avatar when storageMode is selfHost but no URL configured', () => {
133
+ mockIsDesktop = true;
134
+ const mockAvatar = '/api/avatar.png';
135
+
136
+ act(() => {
137
+ useUserStore.setState({ user: { avatar: mockAvatar } as any });
138
+ useElectronStore.setState({
139
+ dataSyncConfig: { remoteServerUrl: '', storageMode: 'selfHost' },
140
+ });
141
+ });
142
+
143
+ const { result } = renderHook(() => useUserAvatar());
144
+
145
+ // In selfHost mode with empty URL, avatar is not prepended
127
146
  expect(result.current).toBe(mockAvatar);
128
147
  });
129
148
  });
@@ -1,12 +1,28 @@
1
+ import { OFFICIAL_URL } from '@lobechat/const';
2
+
1
3
  import { type ElectronState } from '../initialState';
2
4
 
3
5
  const isSyncActive = (s: ElectronState) => s.dataSyncConfig.active;
4
6
 
5
7
  const storageMode = (s: ElectronState) => s.dataSyncConfig.storageMode;
6
- const remoteServerUrl = (s: ElectronState) => s.dataSyncConfig.remoteServerUrl || '';
8
+
9
+ /**
10
+ * Returns the effective remote server URL based on storage mode:
11
+ * - Cloud mode: returns OFFICIAL_URL
12
+ * - SelfHost mode: returns the configured remoteServerUrl
13
+ */
14
+ const remoteServerUrl = (s: ElectronState) =>
15
+ s.dataSyncConfig.storageMode === 'cloud' ? OFFICIAL_URL : s.dataSyncConfig.remoteServerUrl || '';
16
+
17
+ /**
18
+ * Returns the raw remoteServerUrl from config without transformation.
19
+ * Use this when you need the original configured value (e.g., for editing forms).
20
+ */
21
+ const rawRemoteServerUrl = (s: ElectronState) => s.dataSyncConfig.remoteServerUrl || '';
7
22
 
8
23
  export const electronSyncSelectors = {
9
24
  isSyncActive,
25
+ rawRemoteServerUrl,
10
26
  remoteServerUrl,
11
27
  storageMode,
12
28
  };
@@ -84,6 +84,31 @@ describe('SettingsAction', () => {
84
84
  expect.any(AbortSignal),
85
85
  );
86
86
  });
87
+
88
+ it('should include field in diffs when user resets it to default value', async () => {
89
+ const { result } = renderHook(() => useUserStore());
90
+
91
+ // First, set memory.enabled to false (non-default value)
92
+ await act(async () => {
93
+ await result.current.setSettings({ memory: { enabled: false } });
94
+ });
95
+
96
+ expect(userService.updateUserSettings).toHaveBeenLastCalledWith(
97
+ expect.objectContaining({ memory: { enabled: false } }),
98
+ expect.any(AbortSignal),
99
+ );
100
+
101
+ // Then, reset memory.enabled back to true (default value)
102
+ // This should still include memory in the diffs to override the previously saved value
103
+ await act(async () => {
104
+ await result.current.setSettings({ memory: { enabled: true } });
105
+ });
106
+
107
+ expect(userService.updateUserSettings).toHaveBeenLastCalledWith(
108
+ expect.objectContaining({ memory: { enabled: true } }),
109
+ expect.any(AbortSignal),
110
+ );
111
+ });
87
112
  });
88
113
 
89
114
  describe('updateDefaultAgent', () => {
@@ -103,6 +103,17 @@ export const createSettingsSlice: StateCreator<
103
103
  if (isEqual(prevSetting, nextSettings)) return;
104
104
 
105
105
  const diffs = difference(nextSettings, defaultSettings);
106
+
107
+ // When user resets a field to default value, we need to explicitly include it in diffs
108
+ // to override the previously saved non-default value in the backend
109
+ const changedFields = difference(nextSettings, prevSetting);
110
+ for (const key of Object.keys(changedFields)) {
111
+ // Only handle fields that were previously set by user (exist in prevSetting)
112
+ if (key in prevSetting && !(key in diffs)) {
113
+ (diffs as any)[key] = (nextSettings as any)[key];
114
+ }
115
+ }
116
+
106
117
  set({ settings: diffs }, false, 'optimistic_updateSettings');
107
118
 
108
119
  const abortController = get().internal_createSignal();
@@ -1 +0,0 @@
1
- export const TITLE_BAR_HEIGHT = 30;