diff --git a/apps/electron/src/main/index.ts b/apps/electron/src/main/index.ts index 03d2c60..754969f 100644 --- a/apps/electron/src/main/index.ts +++ b/apps/electron/src/main/index.ts @@ -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 @@ -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) @@ -216,6 +223,8 @@ app.on('before-quit', () => { return } + // 注销全局快捷键 + unregisterShortcuts() // 中止所有活跃的 Agent 和 Chat 子进程 stopAllAgents() stopAllGenerations() diff --git a/apps/electron/src/main/ipc.ts b/apps/electron/src/main/ipc.ts index 1073e2f..226edec 100644 --- a/apps/electron/src/main/ipc.ts +++ b/apps/electron/src/main/ipc.ts @@ -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' @@ -471,6 +472,31 @@ export function registerIpcHandlers(): void { }) }) + // 注册全局快捷键 + ipcMain.handle( + SETTINGS_IPC_CHANNELS.REGISTER_SHORTCUTS, + async (): Promise => { + return registerShortcuts() + } + ) + + // 注销全局快捷键 + ipcMain.handle( + SETTINGS_IPC_CHANNELS.UNREGISTER_SHORTCUTS, + async (): Promise => { + unregisterShortcuts() + return true + } + ) + + // 验证快捷键是否可用 + ipcMain.handle( + SETTINGS_IPC_CHANNELS.VALIDATE_SHORTCUT, + async (_, accelerator: string): Promise => { + return validateShortcut(accelerator) + } + ) + // ===== 环境检测相关 ===== // 执行环境检测 diff --git a/apps/electron/src/main/lib/global-shortcut-service.ts b/apps/electron/src/main/lib/global-shortcut-service.ts new file mode 100644 index 0000000..59978a2 --- /dev/null +++ b/apps/electron/src/main/lib/global-shortcut-service.ts @@ -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 + } +} diff --git a/apps/electron/src/main/lib/settings-service.ts b/apps/electron/src/main/lib/settings-service.ts index cd96d33..9fc5248 100644 --- a/apps/electron/src/main/lib/settings-service.ts +++ b/apps/electron/src/main/lib/settings-service.ts @@ -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' /** @@ -24,6 +24,8 @@ export function getSettings(): AppSettings { onboardingCompleted: false, environmentCheckSkipped: false, notificationsEnabled: true, + chatShortcut: DEFAULT_CHAT_SHORTCUT, + agentShortcut: DEFAULT_AGENT_SHORTCUT, } } @@ -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) @@ -47,6 +51,8 @@ export function getSettings(): AppSettings { onboardingCompleted: false, environmentCheckSkipped: false, notificationsEnabled: true, + chatShortcut: DEFAULT_CHAT_SHORTCUT, + agentShortcut: DEFAULT_AGENT_SHORTCUT, } } } diff --git a/apps/electron/src/preload/index.ts b/apps/electron/src/preload/index.ts index 20c563f..b6320b2 100644 --- a/apps/electron/src/preload/index.ts +++ b/apps/electron/src/preload/index.ts @@ -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 接口定义 @@ -204,6 +204,21 @@ export interface ElectronAPI { /** 订阅系统主题变化事件(返回清理函数) */ onSystemThemeChanged: (callback: (isDark: boolean) => void) => () => void + /** 注册全局快捷键 */ + registerShortcuts: () => Promise + + /** 注销全局快捷键 */ + unregisterShortcuts: () => Promise + + /** 验证快捷键是否可用 */ + validateShortcut: (accelerator: string) => Promise + + /** 订阅 Chat 快捷键触发事件(返回清理函数) */ + onChatShortcut: (callback: (behavior: ShortcutBehavior) => void) => () => void + + /** 订阅 Agent 快捷键触发事件(返回清理函数) */ + onAgentShortcut: (callback: (behavior: ShortcutBehavior) => void) => () => void + // ===== 环境检测相关 ===== /** 执行环境检测 */ @@ -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) diff --git a/apps/electron/src/renderer/atoms/settings-tab.ts b/apps/electron/src/renderer/atoms/settings-tab.ts index ebcf90e..1bdc583 100644 --- a/apps/electron/src/renderer/atoms/settings-tab.ts +++ b/apps/electron/src/renderer/atoms/settings-tab.ts @@ -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('channels') diff --git a/apps/electron/src/renderer/atoms/shortcut-atoms.ts b/apps/electron/src/renderer/atoms/shortcut-atoms.ts new file mode 100644 index 0000000..234a89f --- /dev/null +++ b/apps/electron/src/renderer/atoms/shortcut-atoms.ts @@ -0,0 +1,63 @@ +/** + * 快捷键状态管理 + */ + +import { atom } from 'jotai' +import type { ShortcutConfig } from '../../types' + +/** + * Chat 快捷键配置 + */ +export const chatShortcutAtom = atom(null) + +/** + * Agent 快捷键配置 + */ +export const agentShortcutAtom = atom(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 => { + return window.electronAPI.validateShortcut(accelerator) + } +) diff --git a/apps/electron/src/renderer/components/settings/SettingsPanel.tsx b/apps/electron/src/renderer/components/settings/SettingsPanel.tsx index 2a551ce..a8bbc88 100644 --- a/apps/electron/src/renderer/components/settings/SettingsPanel.tsx +++ b/apps/electron/src/renderer/components/settings/SettingsPanel.tsx @@ -9,7 +9,7 @@ import * as React from 'react' import { useAtom, useAtomValue } from 'jotai' import { cn } from '@/lib/utils' -import { Settings, Radio, Palette, Info, Plug, Globe, BookOpen, Brain } from 'lucide-react' +import { Settings, Radio, Palette, Info, Plug, Globe, BookOpen, Brain, Keyboard } from 'lucide-react' import { ScrollArea } from '@/components/ui/scroll-area' import { settingsTabAtom } from '@/atoms/settings-tab' import type { SettingsTab } from '@/atoms/settings-tab' @@ -23,6 +23,7 @@ import { AppearanceSettings } from './AppearanceSettings' import { AboutSettings } from './AboutSettings' import { AgentSettings } from './AgentSettings' import { PromptSettings } from './PromptSettings' +import { ShortcutSettings } from './ShortcutSettings' import { MemorySettings } from './MemorySettings' /** 设置 Tab 定义 */ @@ -37,6 +38,7 @@ const BASE_TABS: TabItem[] = [ { id: 'general', label: '通用', icon: }, { id: 'channels', label: '渠道', icon: }, { id: 'prompts', label: '提示词', icon: }, + { id: 'shortcuts', label: '快捷键', icon: }, { id: 'proxy', label: '代理', icon: }, ] @@ -59,6 +61,8 @@ function renderTabContent(tab: SettingsTab): React.ReactElement { return case 'prompts': return + case 'shortcuts': + return case 'proxy': return case 'agent': diff --git a/apps/electron/src/renderer/components/settings/ShortcutSettings.tsx b/apps/electron/src/renderer/components/settings/ShortcutSettings.tsx new file mode 100644 index 0000000..4e9c13e --- /dev/null +++ b/apps/electron/src/renderer/components/settings/ShortcutSettings.tsx @@ -0,0 +1,151 @@ +/** + * ShortcutSettings - 快捷键设置页面 + */ + +import * as React from 'react' +import { useAtom, useSetAtom } from 'jotai' +import { + SettingsSection, + SettingsCard, + SettingsRow, + SettingsToggle, + SettingsSelect, +} from './primitives' +import { ShortcutRecorder } from './primitives/ShortcutRecorder' +import { + chatShortcutAtom, + agentShortcutAtom, + updateChatShortcutAtom, + updateAgentShortcutAtom, +} from '@/atoms/shortcut-atoms' +import type { ShortcutBehavior } from '../../../types' + +export function ShortcutSettings(): React.ReactElement { + const [chatShortcut] = useAtom(chatShortcutAtom) + const [agentShortcut] = useAtom(agentShortcutAtom) + const updateChatShortcut = useSetAtom(updateChatShortcutAtom) + const updateAgentShortcut = useSetAtom(updateAgentShortcutAtom) + + const handleChatAcceleratorChange = (accelerator: string) => { + if (!chatShortcut) return + updateChatShortcut({ ...chatShortcut, accelerator }) + } + + const handleChatBehaviorChange = (value: string) => { + if (!chatShortcut) return + updateChatShortcut({ ...chatShortcut, behavior: value as ShortcutBehavior }) + } + + const handleChatEnabledChange = (enabled: boolean) => { + if (!chatShortcut) return + updateChatShortcut({ ...chatShortcut, enabled }) + } + + const handleAgentAcceleratorChange = (accelerator: string) => { + if (!agentShortcut) return + updateAgentShortcut({ ...agentShortcut, accelerator }) + } + + const handleAgentBehaviorChange = (value: string) => { + if (!agentShortcut) return + updateAgentShortcut({ ...agentShortcut, behavior: value as ShortcutBehavior }) + } + + const handleAgentEnabledChange = (enabled: boolean) => { + if (!agentShortcut) return + updateAgentShortcut({ ...agentShortcut, enabled }) + } + + const validateChatShortcut = async (accelerator: string): Promise => { + // 检查是否与 Agent 快捷键冲突 + if (agentShortcut?.enabled && agentShortcut.accelerator === accelerator) { + return false + } + // 检查系统占用 + return window.electronAPI.validateShortcut(accelerator) + } + + const validateAgentShortcut = async (accelerator: string): Promise => { + // 检查是否与 Chat 快捷键冲突 + if (chatShortcut?.enabled && chatShortcut.accelerator === accelerator) { + return false + } + // 检查系统占用 + return window.electronAPI.validateShortcut(accelerator) + } + + const behaviorOptions = [ + { value: 'new-conversation', label: '创建新对话' }, + { value: 'current-conversation', label: '打开当前对话' }, + ] + + return ( + + {/* Chat 快捷键 */} + +
+

Chat 模式快捷键

+

快速打开 Chat 对话

+
+ + + + + + + + +
+ + {/* Agent 快捷键 */} + +
+

Agent 模式快捷键

+

快速打开 Agent 会话

+
+ + + + + + + + +
+ +
+

提示:macOS 需要授予辅助功能权限才能使用全局快捷键。

+

如果快捷键不生效,请在系统偏好设置中检查权限。

+
+
+ ) +} diff --git a/apps/electron/src/renderer/components/settings/index.ts b/apps/electron/src/renderer/components/settings/index.ts index f18eb29..b322e47 100644 --- a/apps/electron/src/renderer/components/settings/index.ts +++ b/apps/electron/src/renderer/components/settings/index.ts @@ -7,5 +7,6 @@ export * from './ChannelSettings' export * from './ChannelForm' export * from './GeneralSettings' export * from './AppearanceSettings' +export * from './ShortcutSettings' export * from './AboutSettings' export * from './primitives' diff --git a/apps/electron/src/renderer/components/settings/primitives/ShortcutRecorder.tsx b/apps/electron/src/renderer/components/settings/primitives/ShortcutRecorder.tsx new file mode 100644 index 0000000..573c744 --- /dev/null +++ b/apps/electron/src/renderer/components/settings/primitives/ShortcutRecorder.tsx @@ -0,0 +1,125 @@ +/** + * ShortcutRecorder - 快捷键录制组件 + * + * 监听键盘输入并格式化为 Electron accelerator 格式。 + */ + +import * as React from 'react' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { X } from 'lucide-react' + +interface ShortcutRecorderProps { + value: string + onChange: (accelerator: string) => void + onValidate?: (accelerator: string) => Promise + placeholder?: string +} + +/** + * 格式化快捷键显示 + * 将 Electron accelerator 格式转换为更友好的显示格式 + */ +function formatAccelerator(accelerator: string): string { + if (!accelerator) return '' + + const isMac = navigator.platform.toLowerCase().includes('mac') + + return accelerator + .split('+') + .map((part) => { + switch (part) { + case 'CommandOrControl': + return isMac ? 'Cmd' : 'Ctrl' + case 'Command': + return 'Cmd' + case 'Control': + return 'Ctrl' + case 'Alt': + return isMac ? 'Option' : 'Alt' + case 'Shift': + return 'Shift' + default: + return part + } + }) + .join('+') +} + +export function ShortcutRecorder({ + value, + onChange, + onValidate, + placeholder = '按下快捷键组合...', +}: ShortcutRecorderProps): React.ReactElement { + const [isRecording, setIsRecording] = React.useState(false) + const [isValid, setIsValid] = React.useState(true) + const inputRef = React.useRef(null) + + const handleKeyDown = React.useCallback( + async (e: React.KeyboardEvent) => { + if (!isRecording) return + + e.preventDefault() + e.stopPropagation() + + // 构建 accelerator 字符串 + const modifiers: string[] = [] + // 分别检测 Command 和 Control 键,允许用户区分它们 + if (e.metaKey) modifiers.push('Command') + if (e.ctrlKey) modifiers.push('Control') + if (e.shiftKey) modifiers.push('Shift') + if (e.altKey) modifiers.push('Alt') + + // 获取主键 + let key = e.key + if (key === ' ') key = 'Space' + if (key.length === 1) key = key.toUpperCase() + + // 忽略单独的修饰键 + if (['Control', 'Meta', 'Shift', 'Alt'].includes(key)) return + + const accelerator = [...modifiers, key].join('+') + + // 验证快捷键 + if (onValidate) { + const valid = await onValidate(accelerator) + setIsValid(valid) + if (!valid) return + } + + onChange(accelerator) + setIsRecording(false) + inputRef.current?.blur() + }, + [isRecording, onChange, onValidate] + ) + + const handleClear = () => { + onChange('') + setIsValid(true) + } + + return ( +
+ setIsRecording(true)} + onBlur={() => setIsRecording(false)} + onKeyDown={handleKeyDown} + placeholder={placeholder} + readOnly + className={!isValid ? 'border-red-500' : ''} + /> + {value && ( + + )} + {!isValid && ( + 快捷键已被占用 + )} +
+ ) +} diff --git a/apps/electron/src/renderer/main.tsx b/apps/electron/src/renderer/main.tsx index 54d520a..ff06412 100644 --- a/apps/electron/src/renderer/main.tsx +++ b/apps/electron/src/renderer/main.tsx @@ -28,6 +28,17 @@ import { notificationsEnabledAtom, initializeNotifications, } from './atoms/notifications' +import { loadShortcutsAtom } from './atoms/shortcut-atoms' +import { appModeAtom } from './atoms/app-mode' +import { activeViewAtom } from './atoms/active-view' +import { + conversationsAtom, + currentConversationIdAtom, +} from './atoms/chat-atoms' +import { + agentSessionsAtom, + currentAgentSessionIdAtom, +} from './atoms/agent-atoms' import { useGlobalAgentListeners } from './hooks/useGlobalAgentListeners' import { Toaster } from './components/ui/sonner' import { UpdateDialog } from './components/settings/UpdateDialog' @@ -158,6 +169,119 @@ function NotificationsInitializer(): null { return null } +/** + * 快捷键初始化组件 + * + * 负责加载快捷键配置并监听快捷键触发事件。 + */ +function ShortcutInitializer(): null { + const setAppMode = useSetAtom(appModeAtom) + const setActiveView = useSetAtom(activeViewAtom) + const setCurrentConversationId = useSetAtom(currentConversationIdAtom) + const setCurrentAgentSessionId = useSetAtom(currentAgentSessionIdAtom) + const setConversations = useSetAtom(conversationsAtom) + const setAgentSessions = useSetAtom(agentSessionsAtom) + const conversations = useAtomValue(conversationsAtom) + const agentSessions = useAtomValue(agentSessionsAtom) + const currentConversationId = useAtomValue(currentConversationIdAtom) + const currentAgentSessionId = useAtomValue(currentAgentSessionIdAtom) + const currentWorkspaceId = useAtomValue(currentAgentWorkspaceIdAtom) + const loadShortcuts = useSetAtom(loadShortcutsAtom) + + // Task 12: 加载快捷键配置 + useEffect(() => { + loadShortcuts() + }, [loadShortcuts]) + + // Task 11: 监听快捷键触发事件 + useEffect(() => { + const handleChatShortcut = async (behavior: 'new-conversation' | 'current-conversation'): Promise => { + // 切换到 Chat 模式 + setAppMode('chat') + setActiveView('conversations') + + if (behavior === 'new-conversation') { + // 创建新对话 + const newConv = await window.electronAPI.createConversation() + setCurrentConversationId(newConv.id) + + // 刷新对话列表,确保新对话立即显示 + const updatedList = await window.electronAPI.listConversations() + setConversations(updatedList) + } else { + // 打开当前对话 + if (conversations.length === 0) { + // 没有任何对话,创建新的 + const newConv = await window.electronAPI.createConversation() + setCurrentConversationId(newConv.id) + + // 刷新对话列表 + const updatedList = await window.electronAPI.listConversations() + setConversations(updatedList) + } else if (!currentConversationId) { + // 有对话但没有选中,打开第一个 + setCurrentConversationId(conversations[0]!.id) + } + // 如果已经有选中的对话,保持不变 + } + } + + const handleAgentShortcut = async (behavior: 'new-conversation' | 'current-conversation'): Promise => { + // 切换到 Agent 模式 + setAppMode('agent') + setActiveView('conversations') + + if (behavior === 'new-conversation') { + // 创建新会话,传递当前工作区 ID + const newSession = await window.electronAPI.createAgentSession( + undefined, + undefined, + currentWorkspaceId || undefined + ) + setCurrentAgentSessionId(newSession.id) + + // 刷新会话列表,确保新会话立即显示 + const updatedList = await window.electronAPI.listAgentSessions() + setAgentSessions(updatedList) + } else { + // 打开当前会话 + // 过滤出当前工作区的会话 + const workspaceSessions = agentSessions.filter( + (s) => s.workspaceId === currentWorkspaceId + ) + + if (workspaceSessions.length === 0) { + // 当前工作区没有会话,创建新的 + const newSession = await window.electronAPI.createAgentSession( + undefined, + undefined, + currentWorkspaceId || undefined + ) + setCurrentAgentSessionId(newSession.id) + + // 刷新会话列表 + const updatedList = await window.electronAPI.listAgentSessions() + setAgentSessions(updatedList) + } else if (!currentAgentSessionId) { + // 当前工作区有会话但没有选中,打开第一个 + setCurrentAgentSessionId(workspaceSessions[0]!.id) + } + // 如果已经有选中的会话,保持不变 + } + } + + const cleanupChat = window.electronAPI.onChatShortcut(handleChatShortcut) + const cleanupAgent = window.electronAPI.onAgentShortcut(handleAgentShortcut) + + return () => { + cleanupChat() + cleanupAgent() + } + }, [setAppMode, setActiveView, setCurrentConversationId, setCurrentAgentSessionId, setConversations, setAgentSessions, conversations, agentSessions, currentConversationId, currentAgentSessionId, currentWorkspaceId]) + + return null +} + /** * Agent IPC 监听器初始化组件 * @@ -174,6 +298,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render( + diff --git a/apps/electron/src/types/settings.ts b/apps/electron/src/types/settings.ts index 19bb710..b681bbb 100644 --- a/apps/electron/src/types/settings.ts +++ b/apps/electron/src/types/settings.ts @@ -12,6 +12,33 @@ export type ThemeMode = 'light' | 'dark' | 'system' /** 默认主题模式 */ export const DEFAULT_THEME_MODE: ThemeMode = 'dark' +/** 快捷键行为模式 */ +export type ShortcutBehavior = 'new-conversation' | 'current-conversation' + +/** 快捷键配置 */ +export interface ShortcutConfig { + /** 快捷键组合(如 'CommandOrControl+Shift+C') */ + accelerator: string + /** 是否启用 */ + enabled: boolean + /** 行为模式 */ + behavior: ShortcutBehavior +} + +/** 默认 Chat 快捷键配置 */ +export const DEFAULT_CHAT_SHORTCUT: ShortcutConfig = { + accelerator: 'CommandOrControl+Shift+C', + enabled: true, + behavior: 'new-conversation', +} + +/** 默认 Agent 快捷键配置 */ +export const DEFAULT_AGENT_SHORTCUT: ShortcutConfig = { + accelerator: 'CommandOrControl+Shift+A', + enabled: true, + behavior: 'new-conversation', +} + /** 应用设置 */ export interface AppSettings { /** 主题模式 */ @@ -30,6 +57,10 @@ export interface AppSettings { lastEnvironmentCheck?: EnvironmentCheckResult /** 是否启用桌面通知 */ notificationsEnabled?: boolean + /** Chat 模式快捷键配置 */ + chatShortcut?: ShortcutConfig + /** Agent 模式快捷键配置 */ + agentShortcut?: ShortcutConfig } /** 设置 IPC 通道 */ @@ -38,4 +69,7 @@ export const SETTINGS_IPC_CHANNELS = { UPDATE: 'settings:update', GET_SYSTEM_THEME: 'settings:get-system-theme', ON_SYSTEM_THEME_CHANGED: 'settings:system-theme-changed', + REGISTER_SHORTCUTS: 'settings:register-shortcuts', + UNREGISTER_SHORTCUTS: 'settings:unregister-shortcuts', + VALIDATE_SHORTCUT: 'settings:validate-shortcut', } as const