Skip to content

Commit a060901

Browse files
authored
✨ feat: support double-click to open multi agent window on the desktop (#9331)
* feat: add single pannel * feat: add openTopicInNewWindow to global windows * feat: use ueIsSingleMode hook to replace useSearchParams judge * feat: add session pannel double click & drag create new window * feat: add supensed out in SideBar * fix: update test.ts * feat: add ts define * feat: loading singlemode not render draggablePannel
1 parent 7a34c8b commit a060901

File tree

16 files changed

+489
-29
lines changed

16 files changed

+489
-29
lines changed

apps/desktop/src/main/appBrowsers.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,55 @@ export const appBrowsers = {
4646
},
4747
} satisfies Record<string, BrowserWindowOpts>;
4848

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+
4999
export type AppBrowsersIdentifiers = keyof typeof appBrowsers;
100+
export type WindowTemplateIdentifiers = keyof typeof windowTemplates;

apps/desktop/src/main/controllers/BrowserWindowsCtr.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { InterceptRouteParams } from '@lobechat/electron-client-ipc';
22
import { extractSubPath, findMatchingRoute } from '~common/routes';
33

4-
import { AppBrowsersIdentifiers, BrowsersIdentifiers } from '@/appBrowsers';
4+
import { AppBrowsersIdentifiers, BrowsersIdentifiers, WindowTemplateIdentifiers } from '@/appBrowsers';
55
import { IpcClientEventSender } from '@/types/ipcClientEvent';
66

77
import { ControllerModule, ipcClientEvent, shortcut } from './index';
@@ -100,6 +100,77 @@ export default class BrowserWindowsCtr extends ControllerModule {
100100
}
101101
}
102102

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+
103174
/**
104175
* Open target window and navigate to specified sub-path
105176
*/

apps/desktop/src/main/core/browser/BrowserManager.ts

Lines changed: 88 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { WebContents } from 'electron';
33

44
import { createLogger } from '@/utils/logger';
55

6-
import { AppBrowsersIdentifiers, appBrowsers } from '../../appBrowsers';
6+
import { AppBrowsersIdentifiers, appBrowsers, WindowTemplate, WindowTemplateIdentifiers, windowTemplates } from '../../appBrowsers';
77
import type { App } from '../App';
88
import type { BrowserWindowOpts } from './Browser';
99
import Browser from './Browser';
@@ -14,9 +14,9 @@ const logger = createLogger('core:BrowserManager');
1414
export class BrowserManager {
1515
app: App;
1616

17-
browsers: Map<AppBrowsersIdentifiers, Browser> = new Map();
17+
browsers: Map<string, Browser> = new Map();
1818

19-
private webContentsMap = new Map<WebContents, AppBrowsersIdentifiers>();
19+
private webContentsMap = new Map<WebContents, string>();
2020

2121
constructor(app: App) {
2222
logger.debug('Initializing BrowserManager');
@@ -51,12 +51,12 @@ export class BrowserManager {
5151
};
5252

5353
broadcastToWindow = <T extends MainBroadcastEventKey>(
54-
identifier: AppBrowsersIdentifiers,
54+
identifier: string,
5555
event: T,
5656
data: MainBroadcastParams<T>,
5757
) => {
5858
logger.debug(`Broadcasting event ${event} to window: ${identifier}`);
59-
this.browsers.get(identifier).broadcast(event, data);
59+
this.browsers.get(identifier)?.broadcast(event, data);
6060
};
6161

6262
/**
@@ -87,13 +87,21 @@ export class BrowserManager {
8787
* @param identifier Window identifier
8888
* @param subPath Sub-path, such as 'agent', 'about', etc.
8989
*/
90-
async redirectToPage(identifier: AppBrowsersIdentifiers, subPath?: string) {
90+
async redirectToPage(identifier: string, subPath?: string) {
9191
try {
9292
// Ensure window is retrieved or created
9393
const browser = this.retrieveByIdentifier(identifier);
9494
browser.hide();
9595

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+
}
97105

98106
// Build complete URL path
99107
const fullPath = subPath ? `${baseRoute}/${subPath}` : baseRoute;
@@ -114,13 +122,75 @@ export class BrowserManager {
114122
/**
115123
* get Browser by identifier
116124
*/
117-
retrieveByIdentifier(identifier: AppBrowsersIdentifiers) {
125+
retrieveByIdentifier(identifier: string) {
118126
const browser = this.browsers.get(identifier);
119127

120128
if (browser) return browser;
121129

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+
});
124194
}
125195

126196
/**
@@ -144,7 +214,7 @@ export class BrowserManager {
144214
* @param options Browser window options
145215
*/
146216
private retrieveOrInitialize(options: BrowserWindowOpts) {
147-
let browser = this.browsers.get(options.identifier as AppBrowsersIdentifiers);
217+
let browser = this.browsers.get(options.identifier);
148218
if (browser) {
149219
logger.debug(`Retrieved existing browser: ${options.identifier}`);
150220
return browser;
@@ -153,7 +223,7 @@ export class BrowserManager {
153223
logger.debug(`Creating new browser: ${options.identifier}`);
154224
browser = new Browser(options, this.app);
155225

156-
const identifier = options.identifier as AppBrowsersIdentifiers;
226+
const identifier = options.identifier;
157227
this.browsers.set(identifier, browser);
158228

159229
// 记录 WebContents 和 identifier 的映射
@@ -166,32 +236,32 @@ export class BrowserManager {
166236

167237
browser.browserWindow.on('show', () => {
168238
if (browser.webContents)
169-
this.webContentsMap.set(browser.webContents, browser.identifier as AppBrowsersIdentifiers);
239+
this.webContentsMap.set(browser.webContents, browser.identifier);
170240
});
171241

172242
return browser;
173243
}
174244

175245
closeWindow(identifier: string) {
176-
const browser = this.browsers.get(identifier as AppBrowsersIdentifiers);
246+
const browser = this.browsers.get(identifier);
177247
browser?.close();
178248
}
179249

180250
minimizeWindow(identifier: string) {
181-
const browser = this.browsers.get(identifier as AppBrowsersIdentifiers);
251+
const browser = this.browsers.get(identifier);
182252
browser?.browserWindow.minimize();
183253
}
184254

185255
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()) {
188258
browser?.browserWindow.unmaximize();
189259
} else {
190260
browser?.browserWindow.maximize();
191261
}
192262
}
193263

194-
getIdentifierByWebContents(webContents: WebContents): AppBrowsersIdentifiers | null {
264+
getIdentifierByWebContents(webContents: WebContents): string | null {
195265
return this.webContentsMap.get(webContents) || null;
196266
}
197267

packages/electron-client-ipc/src/events/windows.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
import { InterceptRouteParams, InterceptRouteResponse } from '../types/route';
22

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+
321
export interface WindowsDispatchEvents {
422
/**
523
* 拦截客户端路由导航请求
@@ -14,4 +32,25 @@ export interface WindowsDispatchEvents {
1432
openDevtools: () => void;
1533

1634
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 };
1756
}

0 commit comments

Comments
 (0)