@lobehub/chat 1.133.6 → 1.134.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/apps/desktop/src/main/appBrowsers.ts +51 -0
  3. package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +72 -1
  4. package/apps/desktop/src/main/core/browser/BrowserManager.ts +88 -18
  5. package/changelog/v1.json +9 -0
  6. package/package.json +1 -1
  7. package/packages/electron-client-ipc/src/events/windows.ts +39 -0
  8. package/src/app/[variants]/(main)/_layout/Desktop/DesktopLayoutContainer.tsx +4 -2
  9. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/index.tsx +3 -1
  10. package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +3 -1
  11. package/src/app/[variants]/(main)/chat/(workspace)/@topic/features/TopicListContent/TopicItem/TopicContent.tsx +25 -1
  12. package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/Actions.tsx +19 -2
  13. package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/index.tsx +27 -1
  14. package/src/app/[variants]/(main)/chat/_layout/Desktop/SessionPanel.tsx +11 -1
  15. package/src/app/[variants]/(main)/chat/features/TogglePanelButton.tsx +6 -0
  16. package/src/config/featureFlags/index.ts +2 -2
  17. package/src/config/featureFlags/schema.test.ts +165 -9
  18. package/src/config/featureFlags/schema.ts +68 -46
  19. package/src/features/ElectronTitlebar/Connection/index.tsx +0 -1
  20. package/src/hooks/useIsSingleMode.test.ts +66 -0
  21. package/src/hooks/useIsSingleMode.ts +29 -0
  22. package/src/server/featureFlags/index.ts +56 -0
  23. package/src/server/modules/EdgeConfig/index.ts +43 -4
  24. package/src/store/global/actions/general.ts +46 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 1.134.0](https://github.com/lobehub/lobe-chat/compare/v1.133.6...v1.134.0)
6
+
7
+ <sup>Released on **2025-10-04**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: Support double-click to open multi agent window on the desktop.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: Support double-click to open multi agent window on the desktop, closes [#9331](https://github.com/lobehub/lobe-chat/issues/9331) ([a060901](https://github.com/lobehub/lobe-chat/commit/a060901))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ### [Version 1.133.6](https://github.com/lobehub/lobe-chat/compare/v1.133.5...v1.133.6)
6
31
 
7
32
  <sup>Released on **2025-10-04**</sup>
@@ -46,4 +46,55 @@ export const appBrowsers = {
46
46
  },
47
47
  } satisfies Record<string, BrowserWindowOpts>;
48
48
 
49
+ // Window templates for multi-instance windows
50
+ export interface WindowTemplate {
51
+ allowMultipleInstances: boolean;
52
+ // Include common BrowserWindow options
53
+ autoHideMenuBar?: boolean;
54
+ baseIdentifier: string;
55
+ basePath: string;
56
+ devTools?: boolean;
57
+ height?: number;
58
+ keepAlive?: boolean;
59
+ minWidth?: number;
60
+ parentIdentifier?: string;
61
+ showOnInit?: boolean;
62
+ title?: string;
63
+ titleBarStyle?: 'hidden' | 'default' | 'hiddenInset' | 'customButtonsOnHover';
64
+ vibrancy?:
65
+ | 'appearance-based'
66
+ | 'content'
67
+ | 'fullscreen-ui'
68
+ | 'header'
69
+ | 'hud'
70
+ | 'menu'
71
+ | 'popover'
72
+ | 'selection'
73
+ | 'sheet'
74
+ | 'sidebar'
75
+ | 'titlebar'
76
+ | 'tooltip'
77
+ | 'under-page'
78
+ | 'under-window'
79
+ | 'window';
80
+ width?: number;
81
+ }
82
+
83
+ export const windowTemplates = {
84
+ chatSingle: {
85
+ allowMultipleInstances: true,
86
+ autoHideMenuBar: true,
87
+ baseIdentifier: 'chatSingle',
88
+ basePath: '/chat',
89
+ height: 600,
90
+ keepAlive: false, // Multi-instance windows don't need to stay alive
91
+ minWidth: 400,
92
+ parentIdentifier: 'chat',
93
+ titleBarStyle: 'hidden',
94
+ vibrancy: 'under-window',
95
+ width: 900,
96
+ },
97
+ } satisfies Record<string, WindowTemplate>;
98
+
49
99
  export type AppBrowsersIdentifiers = keyof typeof appBrowsers;
100
+ export type WindowTemplateIdentifiers = keyof typeof windowTemplates;
@@ -1,7 +1,7 @@
1
1
  import { InterceptRouteParams } from '@lobechat/electron-client-ipc';
2
2
  import { extractSubPath, findMatchingRoute } from '~common/routes';
3
3
 
4
- import { AppBrowsersIdentifiers, BrowsersIdentifiers } from '@/appBrowsers';
4
+ import { AppBrowsersIdentifiers, BrowsersIdentifiers, WindowTemplateIdentifiers } from '@/appBrowsers';
5
5
  import { IpcClientEventSender } from '@/types/ipcClientEvent';
6
6
 
7
7
  import { ControllerModule, ipcClientEvent, shortcut } from './index';
@@ -100,6 +100,77 @@ export default class BrowserWindowsCtr extends ControllerModule {
100
100
  }
101
101
  }
102
102
 
103
+ /**
104
+ * Create a new multi-instance window
105
+ */
106
+ @ipcClientEvent('createMultiInstanceWindow')
107
+ async createMultiInstanceWindow(params: {
108
+ templateId: WindowTemplateIdentifiers;
109
+ path: string;
110
+ uniqueId?: string;
111
+ }) {
112
+ try {
113
+ console.log('[BrowserWindowsCtr] Creating multi-instance window:', params);
114
+
115
+ const result = this.app.browserManager.createMultiInstanceWindow(
116
+ params.templateId,
117
+ params.path,
118
+ params.uniqueId,
119
+ );
120
+
121
+ // Show the window
122
+ result.browser.show();
123
+
124
+ return {
125
+ success: true,
126
+ windowId: result.identifier,
127
+ };
128
+ } catch (error) {
129
+ console.error('[BrowserWindowsCtr] Failed to create multi-instance window:', error);
130
+ return {
131
+ error: error.message,
132
+ success: false,
133
+ };
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Get all windows by template
139
+ */
140
+ @ipcClientEvent('getWindowsByTemplate')
141
+ async getWindowsByTemplate(templateId: string) {
142
+ try {
143
+ const windowIds = this.app.browserManager.getWindowsByTemplate(templateId);
144
+ return {
145
+ success: true,
146
+ windowIds,
147
+ };
148
+ } catch (error) {
149
+ console.error('[BrowserWindowsCtr] Failed to get windows by template:', error);
150
+ return {
151
+ error: error.message,
152
+ success: false,
153
+ };
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Close all windows by template
159
+ */
160
+ @ipcClientEvent('closeWindowsByTemplate')
161
+ async closeWindowsByTemplate(templateId: string) {
162
+ try {
163
+ this.app.browserManager.closeWindowsByTemplate(templateId);
164
+ return { success: true };
165
+ } catch (error) {
166
+ console.error('[BrowserWindowsCtr] Failed to close windows by template:', error);
167
+ return {
168
+ error: error.message,
169
+ success: false,
170
+ };
171
+ }
172
+ }
173
+
103
174
  /**
104
175
  * Open target window and navigate to specified sub-path
105
176
  */
@@ -3,7 +3,7 @@ import { WebContents } from 'electron';
3
3
 
4
4
  import { createLogger } from '@/utils/logger';
5
5
 
6
- import { AppBrowsersIdentifiers, appBrowsers } from '../../appBrowsers';
6
+ import { AppBrowsersIdentifiers, appBrowsers, WindowTemplate, WindowTemplateIdentifiers, windowTemplates } from '../../appBrowsers';
7
7
  import type { App } from '../App';
8
8
  import type { BrowserWindowOpts } from './Browser';
9
9
  import Browser from './Browser';
@@ -14,9 +14,9 @@ const logger = createLogger('core:BrowserManager');
14
14
  export class BrowserManager {
15
15
  app: App;
16
16
 
17
- browsers: Map<AppBrowsersIdentifiers, Browser> = new Map();
17
+ browsers: Map<string, Browser> = new Map();
18
18
 
19
- private webContentsMap = new Map<WebContents, AppBrowsersIdentifiers>();
19
+ private webContentsMap = new Map<WebContents, string>();
20
20
 
21
21
  constructor(app: App) {
22
22
  logger.debug('Initializing BrowserManager');
@@ -51,12 +51,12 @@ export class BrowserManager {
51
51
  };
52
52
 
53
53
  broadcastToWindow = <T extends MainBroadcastEventKey>(
54
- identifier: AppBrowsersIdentifiers,
54
+ identifier: string,
55
55
  event: T,
56
56
  data: MainBroadcastParams<T>,
57
57
  ) => {
58
58
  logger.debug(`Broadcasting event ${event} to window: ${identifier}`);
59
- this.browsers.get(identifier).broadcast(event, data);
59
+ this.browsers.get(identifier)?.broadcast(event, data);
60
60
  };
61
61
 
62
62
  /**
@@ -87,13 +87,21 @@ export class BrowserManager {
87
87
  * @param identifier Window identifier
88
88
  * @param subPath Sub-path, such as 'agent', 'about', etc.
89
89
  */
90
- async redirectToPage(identifier: AppBrowsersIdentifiers, subPath?: string) {
90
+ async redirectToPage(identifier: string, subPath?: string) {
91
91
  try {
92
92
  // Ensure window is retrieved or created
93
93
  const browser = this.retrieveByIdentifier(identifier);
94
94
  browser.hide();
95
95
 
96
- const baseRoute = appBrowsers[identifier].path;
96
+ // Handle both static and dynamic windows
97
+ let baseRoute: string;
98
+ if (identifier in appBrowsers) {
99
+ baseRoute = appBrowsers[identifier as AppBrowsersIdentifiers].path;
100
+ } else {
101
+ // For dynamic windows, extract base route from the browser options
102
+ const browserOptions = browser.options;
103
+ baseRoute = browserOptions.path;
104
+ }
97
105
 
98
106
  // Build complete URL path
99
107
  const fullPath = subPath ? `${baseRoute}/${subPath}` : baseRoute;
@@ -114,13 +122,75 @@ export class BrowserManager {
114
122
  /**
115
123
  * get Browser by identifier
116
124
  */
117
- retrieveByIdentifier(identifier: AppBrowsersIdentifiers) {
125
+ retrieveByIdentifier(identifier: string) {
118
126
  const browser = this.browsers.get(identifier);
119
127
 
120
128
  if (browser) return browser;
121
129
 
122
- logger.debug(`Browser ${identifier} not found, initializing new instance`);
123
- return this.retrieveOrInitialize(appBrowsers[identifier]);
130
+ // Check if it's a static browser
131
+ if (identifier in appBrowsers) {
132
+ logger.debug(`Browser ${identifier} not found, initializing new instance`);
133
+ return this.retrieveOrInitialize(appBrowsers[identifier as AppBrowsersIdentifiers]);
134
+ }
135
+
136
+ throw new Error(`Browser ${identifier} not found and is not a static browser`);
137
+ }
138
+
139
+ /**
140
+ * Create a multi-instance window from template
141
+ * @param templateId Template identifier
142
+ * @param path Full path with query parameters
143
+ * @param uniqueId Optional unique identifier, will be generated if not provided
144
+ * @returns The window identifier and Browser instance
145
+ */
146
+ createMultiInstanceWindow(templateId: WindowTemplateIdentifiers, path: string, uniqueId?: string) {
147
+ const template = windowTemplates[templateId];
148
+ if (!template) {
149
+ throw new Error(`Window template ${templateId} not found`);
150
+ }
151
+
152
+ // Generate unique identifier
153
+ const windowId = uniqueId || `${template.baseIdentifier}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
154
+
155
+ // Create browser options from template
156
+ const browserOpts: BrowserWindowOpts = {
157
+ ...template,
158
+ identifier: windowId,
159
+ path: path,
160
+ };
161
+
162
+ logger.debug(`Creating multi-instance window: ${windowId} with path: ${path}`);
163
+
164
+ const browser = this.retrieveOrInitialize(browserOpts);
165
+
166
+ return {
167
+ identifier: windowId,
168
+ browser: browser,
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Get all windows based on template
174
+ * @param templateId Template identifier
175
+ * @returns Array of window identifiers matching the template
176
+ */
177
+ getWindowsByTemplate(templateId: string): string[] {
178
+ const prefix = `${templateId}_`;
179
+ return Array.from(this.browsers.keys()).filter(id => id.startsWith(prefix));
180
+ }
181
+
182
+ /**
183
+ * Close all windows based on template
184
+ * @param templateId Template identifier
185
+ */
186
+ closeWindowsByTemplate(templateId: string): void {
187
+ const windowIds = this.getWindowsByTemplate(templateId);
188
+ windowIds.forEach(id => {
189
+ const browser = this.browsers.get(id);
190
+ if (browser) {
191
+ browser.close();
192
+ }
193
+ });
124
194
  }
125
195
 
126
196
  /**
@@ -144,7 +214,7 @@ export class BrowserManager {
144
214
  * @param options Browser window options
145
215
  */
146
216
  private retrieveOrInitialize(options: BrowserWindowOpts) {
147
- let browser = this.browsers.get(options.identifier as AppBrowsersIdentifiers);
217
+ let browser = this.browsers.get(options.identifier);
148
218
  if (browser) {
149
219
  logger.debug(`Retrieved existing browser: ${options.identifier}`);
150
220
  return browser;
@@ -153,7 +223,7 @@ export class BrowserManager {
153
223
  logger.debug(`Creating new browser: ${options.identifier}`);
154
224
  browser = new Browser(options, this.app);
155
225
 
156
- const identifier = options.identifier as AppBrowsersIdentifiers;
226
+ const identifier = options.identifier;
157
227
  this.browsers.set(identifier, browser);
158
228
 
159
229
  // 记录 WebContents 和 identifier 的映射
@@ -166,32 +236,32 @@ export class BrowserManager {
166
236
 
167
237
  browser.browserWindow.on('show', () => {
168
238
  if (browser.webContents)
169
- this.webContentsMap.set(browser.webContents, browser.identifier as AppBrowsersIdentifiers);
239
+ this.webContentsMap.set(browser.webContents, browser.identifier);
170
240
  });
171
241
 
172
242
  return browser;
173
243
  }
174
244
 
175
245
  closeWindow(identifier: string) {
176
- const browser = this.browsers.get(identifier as AppBrowsersIdentifiers);
246
+ const browser = this.browsers.get(identifier);
177
247
  browser?.close();
178
248
  }
179
249
 
180
250
  minimizeWindow(identifier: string) {
181
- const browser = this.browsers.get(identifier as AppBrowsersIdentifiers);
251
+ const browser = this.browsers.get(identifier);
182
252
  browser?.browserWindow.minimize();
183
253
  }
184
254
 
185
255
  maximizeWindow(identifier: string) {
186
- const browser = this.browsers.get(identifier as AppBrowsersIdentifiers);
187
- if (browser.browserWindow.isMaximized()) {
256
+ const browser = this.browsers.get(identifier);
257
+ if (browser?.browserWindow.isMaximized()) {
188
258
  browser?.browserWindow.unmaximize();
189
259
  } else {
190
260
  browser?.browserWindow.maximize();
191
261
  }
192
262
  }
193
263
 
194
- getIdentifierByWebContents(webContents: WebContents): AppBrowsersIdentifiers | null {
264
+ getIdentifierByWebContents(webContents: WebContents): string | null {
195
265
  return this.webContentsMap.get(webContents) || null;
196
266
  }
197
267
 
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "features": [
5
+ "Support double-click to open multi agent window on the desktop."
6
+ ]
7
+ },
8
+ "date": "2025-10-04",
9
+ "version": "1.134.0"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "fixes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.133.6",
3
+ "version": "1.134.0",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot 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",
@@ -1,5 +1,23 @@
1
1
  import { InterceptRouteParams, InterceptRouteResponse } from '../types/route';
2
2
 
3
+ export interface CreateMultiInstanceWindowParams {
4
+ templateId: string;
5
+ path: string;
6
+ uniqueId?: string;
7
+ }
8
+
9
+ export interface CreateMultiInstanceWindowResponse {
10
+ success: boolean;
11
+ windowId?: string;
12
+ error?: string;
13
+ }
14
+
15
+ export interface GetWindowsByTemplateResponse {
16
+ success: boolean;
17
+ windowIds?: string[];
18
+ error?: string;
19
+ }
20
+
3
21
  export interface WindowsDispatchEvents {
4
22
  /**
5
23
  * 拦截客户端路由导航请求
@@ -14,4 +32,25 @@ export interface WindowsDispatchEvents {
14
32
  openDevtools: () => void;
15
33
 
16
34
  openSettingsWindow: (tab?: string) => void;
35
+
36
+ /**
37
+ * Create a new multi-instance window
38
+ * @param params Window creation parameters
39
+ * @returns Creation result
40
+ */
41
+ createMultiInstanceWindow: (params: CreateMultiInstanceWindowParams) => CreateMultiInstanceWindowResponse;
42
+
43
+ /**
44
+ * Get all windows by template
45
+ * @param templateId Template identifier
46
+ * @returns List of window identifiers
47
+ */
48
+ getWindowsByTemplate: (templateId: string) => GetWindowsByTemplateResponse;
49
+
50
+ /**
51
+ * Close all windows by template
52
+ * @param templateId Template identifier
53
+ * @returns Operation result
54
+ */
55
+ closeWindowsByTemplate: (templateId: string) => { success: boolean; error?: string };
17
56
  }
@@ -1,6 +1,6 @@
1
1
  import { useTheme } from 'antd-style';
2
2
  import { usePathname } from 'next/navigation';
3
- import { PropsWithChildren, memo } from 'react';
3
+ import { PropsWithChildren, Suspense, memo } from 'react';
4
4
  import { Flexbox } from 'react-layout-kit';
5
5
 
6
6
  import SideBar from './SideBar';
@@ -11,7 +11,9 @@ const DesktopLayoutContainer = memo<PropsWithChildren>(({ children }) => {
11
11
  const hideSideBar = pathname.startsWith('/settings');
12
12
  return (
13
13
  <>
14
- {!hideSideBar && <SideBar />}
14
+ <Suspense>
15
+ {!hideSideBar && <SideBar />}
16
+ </Suspense>
15
17
  <Flexbox
16
18
  style={{
17
19
  background: theme.colorBgLayout,
@@ -6,6 +6,7 @@ import { Suspense, memo } from 'react';
6
6
 
7
7
  import { isDesktop } from '@/const/version';
8
8
  import { useActiveTabKey } from '@/hooks/useActiveTabKey';
9
+ import { useIsSingleMode } from '@/hooks/useIsSingleMode';
9
10
  import { usePinnedAgentState } from '@/hooks/usePinnedAgentState';
10
11
  import { useGlobalStore } from '@/store/global';
11
12
  import { systemStatusSelectors } from '@/store/global/selectors';
@@ -26,11 +27,12 @@ const Top = () => {
26
27
 
27
28
  const Nav = memo(() => {
28
29
  const theme = useTheme();
30
+ const isSingleMode = useIsSingleMode()
29
31
  const inZenMode = useGlobalStore(systemStatusSelectors.inZenMode);
30
32
  const { showPinList } = useServerConfigStore(featureFlagsSelectors);
31
33
 
32
34
  return (
33
- !inZenMode && (
35
+ !inZenMode && !isSingleMode && (
34
36
  <SideNav
35
37
  avatar={
36
38
  <div className={electronStylish.nodrag}>
@@ -48,7 +48,9 @@ const Layout = memo<PropsWithChildren>(({ children }) => {
48
48
  <DesktopLayoutContainer>{children}</DesktopLayoutContainer>
49
49
  ) : (
50
50
  <>
51
- <SideBar />
51
+ <Suspense>
52
+ <SideBar />
53
+ </Suspense>
52
54
  {children}
53
55
  </>
54
56
  )}
@@ -2,6 +2,7 @@ import { ActionIcon, Dropdown, EditableText, Icon, type MenuProps, Text } from '
2
2
  import { App } from 'antd';
3
3
  import { createStyles } from 'antd-style';
4
4
  import {
5
+ ExternalLink,
5
6
  LucideCopy,
6
7
  LucideLoader2,
7
8
  MoreVertical,
@@ -16,8 +17,10 @@ import { Flexbox } from 'react-layout-kit';
16
17
 
17
18
  import BubblesLoading from '@/components/BubblesLoading';
18
19
  import { LOADING_FLAT } from '@/const/message';
20
+ import { isDesktop } from '@/const/version';
19
21
  import { useIsMobile } from '@/hooks/useIsMobile';
20
22
  import { useChatStore } from '@/store/chat';
23
+ import { useGlobalStore } from '@/store/global';
21
24
 
22
25
  const useStyles = createStyles(({ css }) => ({
23
26
  content: css`
@@ -45,6 +48,8 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
45
48
 
46
49
  const mobile = useIsMobile();
47
50
 
51
+ const openTopicInNewWindow = useGlobalStore((s) => s.openTopicInNewWindow);
52
+
48
53
  const [
49
54
  editing,
50
55
  favoriteTopic,
@@ -53,6 +58,7 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
53
58
  autoRenameTopicTitle,
54
59
  duplicateTopic,
55
60
  isLoading,
61
+ activeId,
56
62
  ] = useChatStore((s) => [
57
63
  s.topicRenamingId === id,
58
64
  s.favoriteTopic,
@@ -61,6 +67,7 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
61
67
  s.autoRenameTopicTitle,
62
68
  s.duplicateTopic,
63
69
  s.topicLoadingIds.includes(id),
70
+ s.activeId,
64
71
  ]);
65
72
  const { styles, theme } = useStyles();
66
73
 
@@ -88,6 +95,18 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
88
95
  toggleEditing(true);
89
96
  },
90
97
  },
98
+ ...(isDesktop
99
+ ? [
100
+ {
101
+ icon: <Icon icon={ExternalLink} />,
102
+ key: 'openInNewWindow',
103
+ label: '单独打开页面',
104
+ onClick: () => {
105
+ openTopicInNewWindow(activeId, id);
106
+ },
107
+ },
108
+ ]
109
+ : []),
91
110
  {
92
111
  type: 'divider',
93
112
  },
@@ -134,7 +153,7 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
134
153
  },
135
154
  },
136
155
  ],
137
- [],
156
+ [id, activeId, autoRenameTopicTitle, duplicateTopic, removeTopic, t, toggleEditing, openTopicInNewWindow],
138
157
  );
139
158
 
140
159
  return (
@@ -169,6 +188,11 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
169
188
  <Text
170
189
  className={styles.title}
171
190
  ellipsis={{ rows: 1, tooltip: { placement: 'left', title } }}
191
+ onDoubleClick={() => {
192
+ if (isDesktop) {
193
+ openTopicInNewWindow(activeId, id)
194
+ }
195
+ }}
172
196
  style={{ margin: 0 }}
173
197
  >
174
198
  {title}
@@ -5,6 +5,7 @@ import { ItemType } from 'antd/es/menu/interface';
5
5
  import isEqual from 'fast-deep-equal';
6
6
  import {
7
7
  Check,
8
+ ExternalLink,
8
9
  HardDriveDownload,
9
10
  ListTree,
10
11
  LucideCopy,
@@ -17,8 +18,9 @@ import {
17
18
  import { memo, useMemo } from 'react';
18
19
  import { useTranslation } from 'react-i18next';
19
20
 
20
- import { isServerMode } from '@/const/version';
21
+ import { isDesktop, isServerMode } from '@/const/version';
21
22
  import { configService } from '@/services/config';
23
+ import { useGlobalStore } from '@/store/global';
22
24
  import { useSessionStore } from '@/store/session';
23
25
  import { sessionHelpers } from '@/store/session/helpers';
24
26
  import { sessionGroupSelectors, sessionSelectors } from '@/store/session/selectors';
@@ -41,6 +43,8 @@ const Actions = memo<ActionProps>(({ group, id, openCreateGroupModal, setOpen })
41
43
  const { styles } = useStyles();
42
44
  const { t } = useTranslation('chat');
43
45
 
46
+ const openSessionInNewWindow = useGlobalStore((s) => s.openSessionInNewWindow);
47
+
44
48
  const sessionCustomGroups = useSessionStore(sessionGroupSelectors.sessionGroupItems, isEqual);
45
49
  const [pin, removeSession, pinSession, duplicateSession, updateSessionGroup] = useSessionStore(
46
50
  (s) => {
@@ -82,6 +86,19 @@ const Actions = memo<ActionProps>(({ group, id, openCreateGroupModal, setOpen })
82
86
  duplicateSession(id);
83
87
  },
84
88
  },
89
+ ...(isDesktop
90
+ ? [
91
+ {
92
+ icon: <Icon icon={ExternalLink} />,
93
+ key: 'openInNewWindow',
94
+ label: '单独打开页面',
95
+ onClick: ({ domEvent }: { domEvent: Event }) => {
96
+ domEvent.stopPropagation();
97
+ openSessionInNewWindow(id);
98
+ },
99
+ },
100
+ ]
101
+ : []),
85
102
  {
86
103
  type: 'divider',
87
104
  },
@@ -167,7 +184,7 @@ const Actions = memo<ActionProps>(({ group, id, openCreateGroupModal, setOpen })
167
184
  },
168
185
  ] as ItemType[]
169
186
  ).filter(Boolean),
170
- [id, pin],
187
+ [id, pin, openSessionInNewWindow],
171
188
  );
172
189
 
173
190
  return (