Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/electron/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { stopAllGenerations } from './lib/chat-service'
import { initAutoUpdater, cleanupUpdater } from './lib/updater/auto-updater'
import { startWorkspaceWatcher, stopWorkspaceWatcher } from './lib/workspace-watcher'
import { getIsQuitting, setQuitting, isUpdating } from './lib/app-lifecycle'
import { registerShortcuts, unregisterShortcuts, setMainWindow } from './lib/global-shortcut-service'

let mainWindow: BrowserWindow | null = null

Expand Down Expand Up @@ -177,6 +178,12 @@ app.whenReady().then(async () => {
// Create main window (will be shown when ready)
createWindow()

// 设置主窗口引用并注册全局快捷键
if (mainWindow) {
setMainWindow(mainWindow)
registerShortcuts()
}

// 启动工作区文件监听(Agent MCP/Skills + 文件浏览器自动刷新)
if (mainWindow) {
startWorkspaceWatcher(mainWindow)
Expand Down Expand Up @@ -216,6 +223,8 @@ app.on('before-quit', () => {
return
}

// 注销全局快捷键
unregisterShortcuts()
// 中止所有活跃的 Agent 和 Chat 子进程
stopAllAgents()
stopAllGenerations()
Expand Down
26 changes: 26 additions & 0 deletions apps/electron/src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ import {
import { extractTextFromAttachment } from './lib/document-parser'
import { getUserProfile, updateUserProfile } from './lib/user-profile-service'
import { getSettings, updateSettings } from './lib/settings-service'
import { registerShortcuts, unregisterShortcuts, validateShortcut } from './lib/global-shortcut-service'
import { checkEnvironment } from './lib/environment-checker'
import { getProxySettings, saveProxySettings } from './lib/proxy-settings-service'
import { detectSystemProxy } from './lib/system-proxy-detector'
Expand Down Expand Up @@ -471,6 +472,31 @@ export function registerIpcHandlers(): void {
})
})

// 注册全局快捷键
ipcMain.handle(
SETTINGS_IPC_CHANNELS.REGISTER_SHORTCUTS,
async (): Promise<boolean> => {
return registerShortcuts()
}
)

// 注销全局快捷键
ipcMain.handle(
SETTINGS_IPC_CHANNELS.UNREGISTER_SHORTCUTS,
async (): Promise<boolean> => {
unregisterShortcuts()
return true
}
)

// 验证快捷键是否可用
ipcMain.handle(
SETTINGS_IPC_CHANNELS.VALIDATE_SHORTCUT,
async (_, accelerator: string): Promise<boolean> => {
return validateShortcut(accelerator)
}
)

// ===== 环境检测相关 =====

// 执行环境检测
Expand Down
153 changes: 153 additions & 0 deletions apps/electron/src/main/lib/global-shortcut-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* 全局快捷键服务
*
* 管理全局快捷键的注册、注销和触发处理。
*/

import { globalShortcut, BrowserWindow } from 'electron'
import { getSettings } from './settings-service'
import type { ShortcutConfig } from '../../types'

let mainWindow: BrowserWindow | null = null

/**
* 设置主窗口引用
*/
export function setMainWindow(window: BrowserWindow): void {
mainWindow = window
}

/**
* 显示并聚焦窗口
*/
function showAndFocusWindow(): void {
if (!mainWindow) return

if (mainWindow.isMinimized()) {
mainWindow.restore()
}

mainWindow.show()
mainWindow.focus()
}

/**
* 处理 Chat 快捷键触发
* 每次触发时重新读取最新设置,确保使用最新的 behavior 配置
*/
function handleChatShortcut(): void {
// 重新读取最新设置
const settings = getSettings()
const config = settings.chatShortcut

if (!config || !config.enabled) {
console.log('[快捷键] Chat 快捷键已禁用,忽略触发')
return
}

console.log('[快捷键] Chat 快捷键触发:', config)

showAndFocusWindow()

// 发送 IPC 事件到渲染进程
if (mainWindow) {
mainWindow.webContents.send('shortcut:chat', config.behavior)
}
}

/**
* 处理 Agent 快捷键触发
* 每次触发时重新读取最新设置,确保使用最新的 behavior 配置
*/
function handleAgentShortcut(): void {
// 重新读取最新设置
const settings = getSettings()
const config = settings.agentShortcut

if (!config || !config.enabled) {
console.log('[快捷键] Agent 快捷键已禁用,忽略触发')
return
}

console.log('[快捷键] Agent 快捷键触发:', config)

showAndFocusWindow()

// 发送 IPC 事件到渲染进程
if (mainWindow) {
mainWindow.webContents.send('shortcut:agent', config.behavior)
}
}

/**
* 注册全局快捷键
* 注册前先注销所有旧的快捷键,确保配置更新生效
*/
export function registerShortcuts(): boolean {
try {
// 先注销所有旧的快捷键
unregisterShortcuts()

const settings = getSettings()

// 注册 Chat 快捷键
if (settings.chatShortcut?.enabled && settings.chatShortcut.accelerator) {
const registered = globalShortcut.register(
settings.chatShortcut.accelerator,
handleChatShortcut
)

if (!registered) {
console.error('[快捷键] Chat 快捷键注册失败:', settings.chatShortcut.accelerator)
return false
}

console.log('[快捷键] Chat 快捷键已注册:', settings.chatShortcut.accelerator)
}

// 注册 Agent 快捷键
if (settings.agentShortcut?.enabled && settings.agentShortcut.accelerator) {
const registered = globalShortcut.register(
settings.agentShortcut.accelerator,
handleAgentShortcut
)

if (!registered) {
console.error('[快捷键] Agent 快捷键注册失败:', settings.agentShortcut.accelerator)
return false
}

console.log('[快捷键] Agent 快捷键已注册:', settings.agentShortcut.accelerator)
}

return true
} catch (error) {
console.error('[快捷键] 注册失败:', error)
return false
}
}

/**
* 注销所有全局快捷键
*/
export function unregisterShortcuts(): void {
globalShortcut.unregisterAll()
console.log('[快捷键] 所有快捷键已注销')
}

/**
* 验证快捷键是否可用
*/
export function validateShortcut(accelerator: string): boolean {
try {
// 尝试注册并立即注销
const registered = globalShortcut.register(accelerator, () => {})
if (registered) {
globalShortcut.unregister(accelerator)
return true
}
return false
} catch {
return false
}
}
8 changes: 7 additions & 1 deletion apps/electron/src/main/lib/settings-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { readFileSync, writeFileSync, existsSync } from 'node:fs'
import { getSettingsPath } from './config-paths'
import { DEFAULT_THEME_MODE } from '../../types'
import { DEFAULT_THEME_MODE, DEFAULT_CHAT_SHORTCUT, DEFAULT_AGENT_SHORTCUT } from '../../types'
import type { AppSettings } from '../../types'

/**
Expand All @@ -24,6 +24,8 @@ export function getSettings(): AppSettings {
onboardingCompleted: false,
environmentCheckSkipped: false,
notificationsEnabled: true,
chatShortcut: DEFAULT_CHAT_SHORTCUT,
agentShortcut: DEFAULT_AGENT_SHORTCUT,
}
}

Expand All @@ -39,6 +41,8 @@ export function getSettings(): AppSettings {
environmentCheckSkipped: data.environmentCheckSkipped ?? false,
lastEnvironmentCheck: data.lastEnvironmentCheck,
notificationsEnabled: data.notificationsEnabled ?? true,
chatShortcut: data.chatShortcut || DEFAULT_CHAT_SHORTCUT,
agentShortcut: data.agentShortcut || DEFAULT_AGENT_SHORTCUT,
}
} catch (error) {
console.error('[设置] 读取失败:', error)
Expand All @@ -47,6 +51,8 @@ export function getSettings(): AppSettings {
onboardingCompleted: false,
environmentCheckSkipped: false,
notificationsEnabled: true,
chatShortcut: DEFAULT_CHAT_SHORTCUT,
agentShortcut: DEFAULT_AGENT_SHORTCUT,
}
}
}
Expand Down
41 changes: 40 additions & 1 deletion apps/electron/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ import type {
SystemPromptUpdateInput,
MemoryConfig,
} from '@proma/shared'
import type { UserProfile, AppSettings } from '../types'
import type { UserProfile, AppSettings, ShortcutBehavior } from '../types'

/**
* 暴露给渲染进程的 API 接口定义
Expand Down Expand Up @@ -204,6 +204,21 @@ export interface ElectronAPI {
/** 订阅系统主题变化事件(返回清理函数) */
onSystemThemeChanged: (callback: (isDark: boolean) => void) => () => void

/** 注册全局快捷键 */
registerShortcuts: () => Promise<boolean>

/** 注销全局快捷键 */
unregisterShortcuts: () => Promise<boolean>

/** 验证快捷键是否可用 */
validateShortcut: (accelerator: string) => Promise<boolean>

/** 订阅 Chat 快捷键触发事件(返回清理函数) */
onChatShortcut: (callback: (behavior: ShortcutBehavior) => void) => () => void

/** 订阅 Agent 快捷键触发事件(返回清理函数) */
onAgentShortcut: (callback: (behavior: ShortcutBehavior) => void) => () => void

// ===== 环境检测相关 =====

/** 执行环境检测 */
Expand Down Expand Up @@ -590,6 +605,30 @@ const electronAPI: ElectronAPI = {
return () => { ipcRenderer.removeListener(SETTINGS_IPC_CHANNELS.ON_SYSTEM_THEME_CHANGED, listener) }
},

onChatShortcut: (callback: (behavior: ShortcutBehavior) => void) => {
const listener = (_: unknown, behavior: ShortcutBehavior): void => callback(behavior)
ipcRenderer.on('shortcut:chat', listener)
return () => { ipcRenderer.removeListener('shortcut:chat', listener) }
},

onAgentShortcut: (callback: (behavior: ShortcutBehavior) => void) => {
const listener = (_: unknown, behavior: ShortcutBehavior): void => callback(behavior)
ipcRenderer.on('shortcut:agent', listener)
return () => { ipcRenderer.removeListener('shortcut:agent', listener) }
},

registerShortcuts: () => {
return ipcRenderer.invoke(SETTINGS_IPC_CHANNELS.REGISTER_SHORTCUTS)
},

unregisterShortcuts: () => {
return ipcRenderer.invoke(SETTINGS_IPC_CHANNELS.UNREGISTER_SHORTCUTS)
},

validateShortcut: (accelerator: string) => {
return ipcRenderer.invoke(SETTINGS_IPC_CHANNELS.VALIDATE_SHORTCUT, accelerator)
},

// 环境检测
checkEnvironment: () => {
return ipcRenderer.invoke(ENVIRONMENT_IPC_CHANNELS.CHECK)
Expand Down
2 changes: 1 addition & 1 deletion apps/electron/src/renderer/atoms/settings-tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import { atom } from 'jotai'

export type SettingsTab = 'general' | 'channels' | 'proxy' | 'appearance' | 'about' | 'agent' | 'prompts' | 'memory'
export type SettingsTab = 'general' | 'channels' | 'proxy' | 'appearance' | 'about' | 'agent' | 'prompts' | 'shortcuts' | 'memory'

/** 当前设置标签页(不持久化,每次打开设置默认显示渠道) */
export const settingsTabAtom = atom<SettingsTab>('channels')
63 changes: 63 additions & 0 deletions apps/electron/src/renderer/atoms/shortcut-atoms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* 快捷键状态管理
*/

import { atom } from 'jotai'
import type { ShortcutConfig } from '../../types'

/**
* Chat 快捷键配置
*/
export const chatShortcutAtom = atom<ShortcutConfig | null>(null)

/**
* Agent 快捷键配置
*/
export const agentShortcutAtom = atom<ShortcutConfig | null>(null)

/**
* 加载快捷键配置
*/
export const loadShortcutsAtom = atom(null, async (get, set) => {
const settings = await window.electronAPI.getSettings()
set(chatShortcutAtom, settings.chatShortcut || null)
set(agentShortcutAtom, settings.agentShortcut || null)
})

/**
* 更新 Chat 快捷键
*/
export const updateChatShortcutAtom = atom(
null,
async (get, set, config: ShortcutConfig) => {
await window.electronAPI.updateSettings({ chatShortcut: config })
set(chatShortcutAtom, config)

// 重新注册快捷键
await window.electronAPI.registerShortcuts()
}
)

/**
* 更新 Agent 快捷键
*/
export const updateAgentShortcutAtom = atom(
null,
async (get, set, config: ShortcutConfig) => {
await window.electronAPI.updateSettings({ agentShortcut: config })
set(agentShortcutAtom, config)

// 重新注册快捷键
await window.electronAPI.registerShortcuts()
}
)

/**
* 验证快捷键是否可用
*/
export const validateShortcutAtom = atom(
null,
async (get, set, accelerator: string): Promise<boolean> => {
return window.electronAPI.validateShortcut(accelerator)
}
)
Loading