-
Notifications
You must be signed in to change notification settings - Fork 625
feat: implement floating chat window system with performance optimization #724
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,9 +1,9 @@ | ||||||||||||||||||||||||||||||||||||||||
| import { FloatingButtonWindow } from './FloatingButtonWindow' | ||||||||||||||||||||||||||||||||||||||||
| import { FloatingButtonConfig, FloatingButtonState, DEFAULT_FLOATING_BUTTON_CONFIG } from './types' | ||||||||||||||||||||||||||||||||||||||||
| import { ConfigPresenter } from '../configPresenter' | ||||||||||||||||||||||||||||||||||||||||
| import { ipcMain } from 'electron' | ||||||||||||||||||||||||||||||||||||||||
| import { ipcMain, Menu, app } from 'electron' | ||||||||||||||||||||||||||||||||||||||||
| import { FLOATING_BUTTON_EVENTS } from '@/events' | ||||||||||||||||||||||||||||||||||||||||
| import { handleShowHiddenWindow } from '@/utils' | ||||||||||||||||||||||||||||||||||||||||
| import { presenter } from '../index' | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| export class FloatingButtonPresenter { | ||||||||||||||||||||||||||||||||||||||||
| private floatingWindow: FloatingButtonWindow | null = null | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -48,6 +48,7 @@ export class FloatingButtonPresenter { | |||||||||||||||||||||||||||||||||||||||
| this.config.enabled = false | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.CLICKED) | ||||||||||||||||||||||||||||||||||||||||
| ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.RIGHT_CLICKED) | ||||||||||||||||||||||||||||||||||||||||
| if (this.floatingWindow) { | ||||||||||||||||||||||||||||||||||||||||
| this.floatingWindow.destroy() | ||||||||||||||||||||||||||||||||||||||||
| this.floatingWindow = null | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -107,12 +108,40 @@ export class FloatingButtonPresenter { | |||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||
| private async createFloatingWindow(): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||
| ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.CLICKED) | ||||||||||||||||||||||||||||||||||||||||
| ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.RIGHT_CLICKED) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| ipcMain.on(FLOATING_BUTTON_EVENTS.CLICKED, () => { | ||||||||||||||||||||||||||||||||||||||||
| ipcMain.on(FLOATING_BUTTON_EVENTS.CLICKED, async () => { | ||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| // 触发内置事件处理器 | ||||||||||||||||||||||||||||||||||||||||
| handleShowHiddenWindow(true) | ||||||||||||||||||||||||||||||||||||||||
| } catch {} | ||||||||||||||||||||||||||||||||||||||||
| let floatingButtonPosition: { x: number; y: number; width: number; height: number } | null = | ||||||||||||||||||||||||||||||||||||||||
| null | ||||||||||||||||||||||||||||||||||||||||
| if (this.floatingWindow && this.floatingWindow.exists()) { | ||||||||||||||||||||||||||||||||||||||||
| const buttonWindow = this.floatingWindow.getWindow() | ||||||||||||||||||||||||||||||||||||||||
| if (buttonWindow && !buttonWindow.isDestroyed()) { | ||||||||||||||||||||||||||||||||||||||||
| const bounds = buttonWindow.getBounds() | ||||||||||||||||||||||||||||||||||||||||
| floatingButtonPosition = { | ||||||||||||||||||||||||||||||||||||||||
| x: bounds.x, | ||||||||||||||||||||||||||||||||||||||||
| y: bounds.y, | ||||||||||||||||||||||||||||||||||||||||
| width: bounds.width, | ||||||||||||||||||||||||||||||||||||||||
| height: bounds.height | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| if (floatingButtonPosition) { | ||||||||||||||||||||||||||||||||||||||||
| await presenter.windowPresenter.toggleFloatingChatWindow(floatingButtonPosition) | ||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||
| await presenter.windowPresenter.toggleFloatingChatWindow() | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||
| console.error('Failed to handle floating button click:', error) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| ipcMain.on(FLOATING_BUTTON_EVENTS.RIGHT_CLICKED, () => { | ||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| this.showContextMenu() | ||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||
| console.error('Failed to handle floating button right click:', error) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (!this.floatingWindow) { | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -122,5 +151,86 @@ export class FloatingButtonPresenter { | |||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // 悬浮按钮创建后立即显示 | ||||||||||||||||||||||||||||||||||||||||
| this.floatingWindow.show() | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| this.preCreateFloatingChatWindow() | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private preCreateFloatingChatWindow(): void { | ||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| presenter.windowPresenter.createFloatingChatWindow().catch((error) => { | ||||||||||||||||||||||||||||||||||||||||
| console.error('Failed to pre-create floating chat window:', error) | ||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
| console.log('Started pre-creating floating chat window in background') | ||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||
| console.error('Error starting pre-creation of floating chat window:', error) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private showContextMenu(): void { | ||||||||||||||||||||||||||||||||||||||||
| const template = [ | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| label: '打开主窗口', | ||||||||||||||||||||||||||||||||||||||||
| click: () => { | ||||||||||||||||||||||||||||||||||||||||
| this.openMainWindow() | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| type: 'separator' as const | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| label: '退出应用', | ||||||||||||||||||||||||||||||||||||||||
| click: () => { | ||||||||||||||||||||||||||||||||||||||||
| this.exitApplication() | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const contextMenu = Menu.buildFromTemplate(template) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (this.floatingWindow && this.floatingWindow.exists()) { | ||||||||||||||||||||||||||||||||||||||||
| const buttonWindow = this.floatingWindow.getWindow() | ||||||||||||||||||||||||||||||||||||||||
| if (buttonWindow && !buttonWindow.isDestroyed()) { | ||||||||||||||||||||||||||||||||||||||||
| contextMenu.popup({ window: buttonWindow }) | ||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const mainWindow = presenter.windowPresenter.mainWindow | ||||||||||||||||||||||||||||||||||||||||
| if (mainWindow) { | ||||||||||||||||||||||||||||||||||||||||
| contextMenu.popup({ window: mainWindow }) | ||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||
| contextMenu.popup() | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+169
to
+204
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Localize context menu labels and reuse existing i18n infra Hardcoded Chinese labels break localization. Reuse your i18n helpers (like getContextMenuLabels) or add keys for “Open main window” and “Exit application” to keep UX consistent. Example: - const template = [
- { label: '打开主窗口', click: () => this.openMainWindow() },
- { type: 'separator' as const },
- { label: '退出应用', click: () => this.exitApplication() }
- ]
+ const lang = presenter?.configPresenter?.getLanguage?.() ?? 'en'
+ const labels = { openMain: /* t('menu.openMain') by lang */, exitApp: /* t('menu.exitApp') */ }
+ const template = [
+ { label: labels.openMain, click: () => this.openMainWindow() },
+ { type: 'separator' as const },
+ { label: labels.exitApp, click: () => this.exitApplication() }
+ ]I can wire this to your existing i18n utilities on request.
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private openMainWindow(): void { | ||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| const windowPresenter = presenter.windowPresenter | ||||||||||||||||||||||||||||||||||||||||
| if (windowPresenter) { | ||||||||||||||||||||||||||||||||||||||||
| const mainWindow = windowPresenter.mainWindow | ||||||||||||||||||||||||||||||||||||||||
| if (mainWindow && !mainWindow.isDestroyed()) { | ||||||||||||||||||||||||||||||||||||||||
| if (mainWindow.isMinimized()) { | ||||||||||||||||||||||||||||||||||||||||
| mainWindow.restore() | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| mainWindow.show() | ||||||||||||||||||||||||||||||||||||||||
| mainWindow.focus() | ||||||||||||||||||||||||||||||||||||||||
| console.log('Main window opened from floating button context menu') | ||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||
| windowPresenter.createShellWindow({ initialTab: { url: 'local://chat' } }) | ||||||||||||||||||||||||||||||||||||||||
| console.log('Created new main window from floating button context menu') | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||
| console.error('Failed to open main window from floating button:', error) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private exitApplication(): void { | ||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| console.log('Exiting application from floating button context menu') | ||||||||||||||||||||||||||||||||||||||||
| app.quit() | ||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||
| console.error('Failed to exit application from floating button:', error) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+228
to
235
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Route app exit through WindowPresenter to set isQuitting before window closes Direct app.quit() can race with close handlers that rely on isQuitting. Emit the FORCE_QUIT_APP event instead, which your WindowPresenter already handles to set isQuitting then quit. - private exitApplication(): void {
- try {
- console.log('Exiting application from floating button context menu')
- app.quit()
- } catch (error) {
- console.error('Failed to exit application from floating button:', error)
- }
- }
+ private exitApplication(): void {
+ try {
+ console.log('Exiting application from floating button context menu')
+ // Ensure centralized quit path sets isQuitting before windows receive 'close'
+ const { eventBus } = require('@/eventbus')
+ const { WINDOW_EVENTS } = require('@/events')
+ eventBus.sendToMain(WINDOW_EVENTS.FORCE_QUIT_APP)
+ } catch (error) {
+ console.error('Failed to exit application from floating button:', error)
+ }
+ }Note: add missing imports if your bundler supports them at top-level instead of require(). 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -942,4 +942,40 @@ export class TabPresenter implements ITabPresenter { | |
| } | ||
| } | ||
| } | ||
|
|
||
| registerFloatingWindow(webContentsId: number, webContents: Electron.WebContents): void { | ||
| try { | ||
| console.log(`TabPresenter: Registering floating window as virtual tab, ID: ${webContentsId}`) | ||
| if (this.tabs.has(webContentsId)) { | ||
| console.warn(`TabPresenter: Tab ${webContentsId} already exists, skipping registration`) | ||
| return | ||
| } | ||
| const virtualView = { | ||
| webContents: webContents, | ||
| setVisible: () => {}, | ||
| setBounds: () => {}, | ||
| getBounds: () => ({ x: 0, y: 0, width: 400, height: 600 }) | ||
| } as any | ||
| this.webContentsToTabId.set(webContentsId, webContentsId) | ||
| this.tabs.set(webContentsId, virtualView) | ||
| console.log( | ||
| `TabPresenter: Virtual tab registered successfully for floating window ${webContentsId}` | ||
| ) | ||
| } catch (error) { | ||
| console.error('TabPresenter: Failed to register floating window:', error) | ||
| } | ||
| } | ||
|
|
||
| unregisterFloatingWindow(webContentsId: number): void { | ||
| try { | ||
| console.log(`TabPresenter: Unregistering floating window virtual tab, ID: ${webContentsId}`) | ||
| this.webContentsToTabId.delete(webContentsId) | ||
| this.tabs.delete(webContentsId) | ||
| console.log( | ||
| `TabPresenter: Virtual tab unregistered successfully for floating window ${webContentsId}` | ||
| ) | ||
| } catch (error) { | ||
| console.error('TabPresenter: Failed to unregister floating window:', error) | ||
| } | ||
| } | ||
|
Comment on lines
+946
to
+980
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid storing non-WebContentsView “virtual tabs” in Map<number, WebContentsView> Using a plain object cast as any risks runtime/typing issues if later code assumes a real WebContentsView (e.g., addChildView, setBorderRadius). Recommend isolating virtual entries in a separate map or introducing a safe abstraction instead of widening via any. Proposed direction:
🤖 Prompt for AI Agents |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Pass-through of button bounds is good; consider multi-display correctness
You pass the floating button’s bounds to help position the chat window near the button. However, FloatingChatWindow.calculatePosition() uses screen.getPrimaryDisplay(), which can misplace the window when the button is on a non-primary monitor. Prefer deriving the display nearest the button center and using that display’s workArea.
Additional change needed in FloatingChatWindow (for reference):
🤖 Prompt for AI Agents