diff --git a/apps/electron/src/main/ipc.ts b/apps/electron/src/main/ipc.ts index 1073e2f..16051e3 100644 --- a/apps/electron/src/main/ipc.ts +++ b/apps/electron/src/main/ipc.ts @@ -471,6 +471,29 @@ export function registerIpcHandlers(): void { }) }) + // 设置 Electron 原生缩放因子(全局模式) + ipcMain.handle( + SETTINGS_IPC_CHANNELS.SET_ZOOM_FACTOR, + async (event, zoomFactor: number): Promise => { + const win = BrowserWindow.fromWebContents(event.sender) + if (win) { + win.webContents.setZoomFactor(zoomFactor) + } + } + ) + + // 获取当前 Electron 原生缩放因子 + ipcMain.handle( + SETTINGS_IPC_CHANNELS.GET_ZOOM_FACTOR, + async (event): Promise => { + const win = BrowserWindow.fromWebContents(event.sender) + if (win) { + return win.webContents.getZoomFactor() + } + return 1.0 + } + ) + // ===== 环境检测相关 ===== // 执行环境检测 diff --git a/apps/electron/src/main/lib/settings-service.ts b/apps/electron/src/main/lib/settings-service.ts index cd96d33..b9a3bbf 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_ZOOM_MODE, DEFAULT_MESSAGE_AREA_ZOOM_LEVEL, DEFAULT_GLOBAL_ZOOM_LEVEL } from '../../types' import type { AppSettings } from '../../types' /** @@ -21,6 +21,9 @@ export function getSettings(): AppSettings { if (!existsSync(filePath)) { return { themeMode: DEFAULT_THEME_MODE, + zoomMode: DEFAULT_ZOOM_MODE, + messageAreaZoomLevel: DEFAULT_MESSAGE_AREA_ZOOM_LEVEL, + globalZoomLevel: DEFAULT_GLOBAL_ZOOM_LEVEL, onboardingCompleted: false, environmentCheckSkipped: false, notificationsEnabled: true, @@ -32,6 +35,9 @@ export function getSettings(): AppSettings { const data = JSON.parse(raw) as Partial return { themeMode: data.themeMode || DEFAULT_THEME_MODE, + zoomMode: data.zoomMode || DEFAULT_ZOOM_MODE, + messageAreaZoomLevel: data.messageAreaZoomLevel ?? DEFAULT_MESSAGE_AREA_ZOOM_LEVEL, + globalZoomLevel: data.globalZoomLevel ?? DEFAULT_GLOBAL_ZOOM_LEVEL, agentChannelId: data.agentChannelId, agentModelId: data.agentModelId, agentWorkspaceId: data.agentWorkspaceId, @@ -44,6 +50,8 @@ export function getSettings(): AppSettings { console.error('[设置] 读取失败:', error) return { themeMode: DEFAULT_THEME_MODE, + zoomMode: DEFAULT_ZOOM_MODE, + messageAreaZoomLevel: DEFAULT_MESSAGE_AREA_ZOOM_LEVEL, onboardingCompleted: false, environmentCheckSkipped: false, notificationsEnabled: true, diff --git a/apps/electron/src/main/menu.ts b/apps/electron/src/main/menu.ts index 123e10d..465a0da 100644 --- a/apps/electron/src/main/menu.ts +++ b/apps/electron/src/main/menu.ts @@ -1,4 +1,5 @@ -import { Menu, shell } from 'electron' +import { Menu, shell, BrowserWindow } from 'electron' +import { getSettings } from './lib/settings-service' export function createApplicationMenu(): Menu { const isMac = process.platform === 'darwin' @@ -58,9 +59,33 @@ export function createApplicationMenu(): Menu { { role: 'forceReload' as const, label: '强制重新加载' }, { role: 'toggleDevTools' as const, label: '切换开发者工具' }, { type: 'separator' as const }, - { role: 'resetZoom' as const, label: '重置缩放' }, - { role: 'zoomIn' as const, label: '放大' }, - { role: 'zoomOut' as const, label: '缩小' }, + { + label: '重置缩放', + accelerator: 'CommandOrControl+0', + click: (_menuItem, browserWindow) => { + if (!browserWindow || !(browserWindow instanceof BrowserWindow)) return + // 统一发送 IPC 事件,由渲染进程根据模式处理 + browserWindow.webContents.send('zoom:reset') + }, + }, + { + label: '放大', + accelerator: 'CommandOrControl+Plus', + click: (_menuItem, browserWindow) => { + if (!browserWindow || !(browserWindow instanceof BrowserWindow)) return + // 统一发送 IPC 事件,由渲染进程根据模式处理 + browserWindow.webContents.send('zoom:in') + }, + }, + { + label: '缩小', + accelerator: 'CommandOrControl+-', + click: (_menuItem, browserWindow) => { + if (!browserWindow || !(browserWindow instanceof BrowserWindow)) return + // 统一发送 IPC 事件,由渲染进程根据模式处理 + browserWindow.webContents.send('zoom:out') + }, + }, { type: 'separator' as const }, { role: 'togglefullscreen' as const, label: '切换全屏' }, ], diff --git a/apps/electron/src/preload/index.ts b/apps/electron/src/preload/index.ts index 20c563f..0ec5022 100644 --- a/apps/electron/src/preload/index.ts +++ b/apps/electron/src/preload/index.ts @@ -204,6 +204,12 @@ export interface ElectronAPI { /** 订阅系统主题变化事件(返回清理函数) */ onSystemThemeChanged: (callback: (isDark: boolean) => void) => () => void + /** 设置 Electron 原生缩放因子(全局模式) */ + setZoomFactor: (zoomFactor: number) => Promise + + /** 获取当前 Electron 原生缩放因子 */ + getZoomFactor: () => Promise + // ===== 环境检测相关 ===== /** 执行环境检测 */ @@ -421,6 +427,17 @@ export interface ElectronAPI { // 工作区文件变化通知 onCapabilitiesChanged: (callback: () => void) => () => void onWorkspaceFilesChanged: (callback: () => void) => () => void + + // ===== 缩放事件订阅(返回清理函数) ===== + + /** 订阅放大事件 */ + onZoomIn: (callback: () => void) => () => void + + /** 订阅缩小事件 */ + onZoomOut: (callback: () => void) => () => void + + /** 订阅重置缩放事件 */ + onZoomReset: (callback: () => void) => () => void } /** @@ -590,6 +607,14 @@ const electronAPI: ElectronAPI = { return () => { ipcRenderer.removeListener(SETTINGS_IPC_CHANNELS.ON_SYSTEM_THEME_CHANGED, listener) } }, + setZoomFactor: (zoomFactor: number) => { + return ipcRenderer.invoke(SETTINGS_IPC_CHANNELS.SET_ZOOM_FACTOR, zoomFactor) + }, + + getZoomFactor: () => { + return ipcRenderer.invoke(SETTINGS_IPC_CHANNELS.GET_ZOOM_FACTOR) + }, + // 环境检测 checkEnvironment: () => { return ipcRenderer.invoke(ENVIRONMENT_IPC_CHANNELS.CHECK) @@ -875,6 +900,26 @@ const electronAPI: ElectronAPI = { getReleaseByTag: (tag) => { return ipcRenderer.invoke(GITHUB_RELEASE_IPC_CHANNELS.GET_RELEASE_BY_TAG, tag) }, + + // ===== 缩放事件订阅 ===== + + onZoomIn: (callback) => { + const listener = (): void => callback() + ipcRenderer.on('zoom:in', listener) + return () => { ipcRenderer.removeListener('zoom:in', listener) } + }, + + onZoomOut: (callback) => { + const listener = (): void => callback() + ipcRenderer.on('zoom:out', listener) + return () => { ipcRenderer.removeListener('zoom:out', listener) } + }, + + onZoomReset: (callback) => { + const listener = (): void => callback() + ipcRenderer.on('zoom:reset', listener) + return () => { ipcRenderer.removeListener('zoom:reset', listener) } + }, } // 将 API 暴露到渲染进程的 window 对象上 diff --git a/apps/electron/src/renderer/atoms/index.ts b/apps/electron/src/renderer/atoms/index.ts index 184f92f..186aa63 100644 --- a/apps/electron/src/renderer/atoms/index.ts +++ b/apps/electron/src/renderer/atoms/index.ts @@ -9,3 +9,4 @@ export * from './settings-tab' export * from './chat-atoms' export * from './user-profile' export * from './theme' +export * from './zoom-atoms' diff --git a/apps/electron/src/renderer/atoms/zoom-atoms.ts b/apps/electron/src/renderer/atoms/zoom-atoms.ts new file mode 100644 index 0000000..38e5ed4 --- /dev/null +++ b/apps/electron/src/renderer/atoms/zoom-atoms.ts @@ -0,0 +1,220 @@ +/** + * 缩放状态原子 + * + * 管理应用缩放模式、消息区域缩放级别和全局缩放级别。 + * - zoomModeAtom: 缩放模式(消息区域/全局),持久化到 ~/.proma/settings.json + * - messageAreaZoomLevelAtom: 消息区域缩放级别(0.5-2.0),持久化到 ~/.proma/settings.json + * - globalZoomLevelAtom: 全局缩放级别(0.5-2.0),持久化到 ~/.proma/settings.json + * + * 使用 localStorage 作为缓存,避免页面加载时闪烁。 + */ + +import { atom } from 'jotai' +import type { ZoomMode } from '../../types' +import { + DEFAULT_ZOOM_MODE, + DEFAULT_MESSAGE_AREA_ZOOM_LEVEL, + DEFAULT_GLOBAL_ZOOM_LEVEL, + ZOOM_MIN, + ZOOM_MAX, + ZOOM_STEP, +} from '../../types/settings' + +/** localStorage 缓存键 */ +const ZOOM_MODE_CACHE_KEY = 'proma-zoom-mode' +const MESSAGE_AREA_ZOOM_LEVEL_CACHE_KEY = 'proma-message-area-zoom-level' +const GLOBAL_ZOOM_LEVEL_CACHE_KEY = 'proma-global-zoom-level' + +/** + * 从 localStorage 读取缓存的缩放模式 + */ +function getCachedZoomMode(): ZoomMode { + try { + const cached = localStorage.getItem(ZOOM_MODE_CACHE_KEY) + if (cached === 'message-area' || cached === 'global') { + return cached + } + } catch { + // localStorage 不可用时忽略 + } + return DEFAULT_ZOOM_MODE +} + +/** + * 从 localStorage 读取缓存的消息区域缩放级别 + */ +function getCachedMessageAreaZoomLevel(): number { + try { + const cached = localStorage.getItem(MESSAGE_AREA_ZOOM_LEVEL_CACHE_KEY) + if (cached) { + const level = Number.parseFloat(cached) + if (!Number.isNaN(level) && level >= ZOOM_MIN && level <= ZOOM_MAX) { + return level + } + } + } catch { + // localStorage 不可用时忽略 + } + return DEFAULT_MESSAGE_AREA_ZOOM_LEVEL +} + +/** + * 从 localStorage 读取缓存的全局缩放级别 + */ +function getCachedGlobalZoomLevel(): number { + try { + const cached = localStorage.getItem(GLOBAL_ZOOM_LEVEL_CACHE_KEY) + if (cached) { + const level = Number.parseFloat(cached) + if (!Number.isNaN(level) && level >= ZOOM_MIN && level <= ZOOM_MAX) { + return level + } + } + } catch { + // localStorage 不可用时忽略 + } + return DEFAULT_GLOBAL_ZOOM_LEVEL +} + +/** + * 缓存缩放模式到 localStorage + */ +function cacheZoomMode(mode: ZoomMode): void { + try { + localStorage.setItem(ZOOM_MODE_CACHE_KEY, mode) + } catch { + // localStorage 不可用时忽略 + } +} + +/** + * 缓存消息区域缩放级别到 localStorage + */ +function cacheMessageAreaZoomLevel(level: number): void { + try { + localStorage.setItem(MESSAGE_AREA_ZOOM_LEVEL_CACHE_KEY, level.toString()) + } catch { + // localStorage 不可用时忽略 + } +} + +/** + * 缓存全局缩放级别到 localStorage + */ +function cacheGlobalZoomLevel(level: number): void { + try { + localStorage.setItem(GLOBAL_ZOOM_LEVEL_CACHE_KEY, level.toString()) + } catch { + // localStorage 不可用时忽略 + } +} + +/** 缩放模式 */ +export const zoomModeAtom = atom(getCachedZoomMode()) + +/** 消息区域缩放级别 */ +export const messageAreaZoomLevelAtom = atom(getCachedMessageAreaZoomLevel()) + +/** 全局缩放级别 */ +export const globalZoomLevelAtom = atom(getCachedGlobalZoomLevel()) + +/** + * 初始化缩放系统 + * + * 从主进程加载设置。 + */ +export async function initializeZoom( + setZoomMode: (mode: ZoomMode) => void, + setMessageAreaZoomLevel: (level: number) => void, + setGlobalZoomLevel: (level: number) => void, +): Promise { + // 从主进程加载持久化设置 + const settings = await window.electronAPI.getSettings() + const mode = settings.zoomMode || DEFAULT_ZOOM_MODE + const messageAreaLevel = settings.messageAreaZoomLevel || DEFAULT_MESSAGE_AREA_ZOOM_LEVEL + const globalLevel = settings.globalZoomLevel || DEFAULT_GLOBAL_ZOOM_LEVEL + + setZoomMode(mode) + setMessageAreaZoomLevel(messageAreaLevel) + setGlobalZoomLevel(globalLevel) + cacheZoomMode(mode) + cacheMessageAreaZoomLevel(messageAreaLevel) + cacheGlobalZoomLevel(globalLevel) + + // 如果是全局模式,设置 Electron 原生缩放 + if (mode === 'global') { + await window.electronAPI.setZoomFactor(globalLevel) + } +} + +/** + * 更新缩放模式并持久化 + * + * 切换到全局模式时,设置 Electron 原生缩放 + * 切换到消息区域模式时,重置 Electron 原生缩放为 100% + */ +export async function updateZoomMode(mode: ZoomMode): Promise { + cacheZoomMode(mode) + await window.electronAPI.updateSettings({ zoomMode: mode }) + + // 根据模式设置 Electron 原生缩放 + if (mode === 'global') { + // 切换到全局模式:应用全局缩放级别 + const settings = await window.electronAPI.getSettings() + const globalLevel = settings.globalZoomLevel || DEFAULT_GLOBAL_ZOOM_LEVEL + await window.electronAPI.setZoomFactor(globalLevel) + } else { + // 切换到消息区域模式:重置 Electron 原生缩放为 100% + await window.electronAPI.setZoomFactor(1.0) + } +} + +/** + * 更新消息区域缩放级别并持久化 + */ +export async function updateMessageAreaZoomLevel(level: number): Promise { + // 确保级别在有效范围内 + const clampedLevel = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, level)) + cacheMessageAreaZoomLevel(clampedLevel) + await window.electronAPI.updateSettings({ messageAreaZoomLevel: clampedLevel }) +} + +/** + * 更新全局缩放级别并持久化 + * + * 使用 Electron 原生缩放(webContents.setZoomFactor),不会出现滚动条 + */ +export async function updateGlobalZoomLevel(level: number): Promise { + // 确保级别在有效范围内 + const clampedLevel = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, level)) + cacheGlobalZoomLevel(clampedLevel) + + // 设置 Electron 原生缩放因子 + await window.electronAPI.setZoomFactor(clampedLevel) + + // 持久化到设置 + await window.electronAPI.updateSettings({ globalZoomLevel: clampedLevel }) +} + +/** + * 增加缩放级别 + */ +export function zoomIn(currentLevel: number): number { + const newLevel = currentLevel + ZOOM_STEP + return Math.min(ZOOM_MAX, Math.round(newLevel * 10) / 10) +} + +/** + * 减少缩放级别 + */ +export function zoomOut(currentLevel: number): number { + const newLevel = currentLevel - ZOOM_STEP + return Math.max(ZOOM_MIN, Math.round(newLevel * 10) / 10) +} + +/** + * 重置缩放级别到 100% + */ +export function resetZoom(): number { + return 1.0 +} diff --git a/apps/electron/src/renderer/components/agent/AgentMessages.tsx b/apps/electron/src/renderer/components/agent/AgentMessages.tsx index 96d97b2..ce3cfec 100644 --- a/apps/electron/src/renderer/components/agent/AgentMessages.tsx +++ b/apps/electron/src/renderer/components/agent/AgentMessages.tsx @@ -44,6 +44,7 @@ import { agentStartedAtAtom, } from '@/atoms/agent-atoms' import { userProfileAtom } from '@/atoms/user-profile' +import { messageAreaZoomLevelAtom, zoomModeAtom } from '@/atoms/zoom-atoms' import { cn } from '@/lib/utils' import type { AgentMessage, RetryAttempt } from '@proma/shared' import type { ToolActivity, AgentStreamState } from '@/atoms/agent-atoms' @@ -479,6 +480,8 @@ export function AgentMessages(): React.ReactElement { const agentStreamingModel = useAtomValue(agentStreamingModelAtom) const retrying = useAtomValue(agentRetryingAtom) const startedAt = useAtomValue(agentStartedAtAtom) + const zoomMode = useAtomValue(zoomModeAtom) + const zoomLevel = useAtomValue(messageAreaZoomLevelAtom) // 获取后台任务列表 const { tasks: backgroundTasks } = useBackgroundTasks(currentSessionId || '') @@ -500,8 +503,11 @@ export function AgentMessages(): React.ReactElement { [messages, userProfile.avatar] ) + // 计算 zoom 样式(仅在消息区域缩放模式下应用) + const zoomStyle = zoomMode === 'message-area' ? { zoom: zoomLevel, transition: 'zoom 0.2s ease-out' } : {} + return ( - + {messages.length === 0 && !streaming ? ( diff --git a/apps/electron/src/renderer/components/chat/ChatMessages.tsx b/apps/electron/src/renderer/components/chat/ChatMessages.tsx index 27a63ba..264177b 100644 --- a/apps/electron/src/renderer/components/chat/ChatMessages.tsx +++ b/apps/electron/src/renderer/components/chat/ChatMessages.tsx @@ -53,6 +53,7 @@ import { hasMoreMessagesAtom, currentConversationIdAtom, } from '@/atoms/chat-atoms' +import { messageAreaZoomLevelAtom, zoomModeAtom } from '@/atoms/zoom-atoms' import { getModelLogo } from '@/lib/model-logo' import { userProfileAtom } from '@/atoms/user-profile' import type { ChatMessage, ChatToolActivity } from '@proma/shared' @@ -229,6 +230,8 @@ export function ChatMessages({ const streamingContent = useAtomValue(streamingContentAtom) const streamingReasoning = useAtomValue(streamingReasoningAtom) const toolActivities = useAtomValue(streamingToolActivitiesAtom) + const zoomMode = useAtomValue(zoomModeAtom) + const zoomLevel = useAtomValue(messageAreaZoomLevelAtom) // 平滑流式输出:将高频 atom 更新转为逐字渲染 const { displayedContent: smoothContent } = useSmoothStream({ @@ -335,8 +338,14 @@ export function ChatMessages({ // 标准消息列表模式 const dividerSet = new Set(contextDividers) + // 计算 zoom 样式(仅在消息区域缩放模式下应用) + const zoomStyle = zoomMode === 'message-area' ? { zoom: zoomLevel, transition: 'zoom 0.2s ease-out' } : {} + return ( - + {/* 滚动到顶部时自动加载更多历史 */} segmentMessages(messages, contextDividers), @@ -260,7 +268,7 @@ export function ParallelChatMessages({ // 如果没有分隔线,使用简单的两列布局 if (segments.length <= 1) { return ( -
+
{/* 加载更多历史消息的旋转器 */} {loadingMore && (
@@ -319,7 +327,7 @@ export function ParallelChatMessages({ // 有分隔线的情况:分段渲染 return ( -
+
{/* 加载更多历史消息的旋转器 */} {loadingMore && } diff --git a/apps/electron/src/renderer/components/settings/AppearanceSettings.tsx b/apps/electron/src/renderer/components/settings/AppearanceSettings.tsx index cf7ade8..8063937 100644 --- a/apps/electron/src/renderer/components/settings/AppearanceSettings.tsx +++ b/apps/electron/src/renderer/components/settings/AppearanceSettings.tsx @@ -6,7 +6,7 @@ */ import * as React from 'react' -import { useAtom } from 'jotai' +import { useAtom, useAtomValue } from 'jotai' import { SettingsSection, SettingsCard, @@ -14,7 +14,8 @@ import { SettingsSegmentedControl, } from './primitives' import { themeModeAtom, updateThemeMode } from '@/atoms/theme' -import type { ThemeMode } from '../../../types' +import { zoomModeAtom, messageAreaZoomLevelAtom, globalZoomLevelAtom, updateZoomMode } from '@/atoms/zoom-atoms' +import type { ThemeMode, ZoomMode } from '../../../types' /** 主题选项 */ const THEME_OPTIONS = [ @@ -23,6 +24,12 @@ const THEME_OPTIONS = [ { value: 'system', label: '跟随系统' }, ] +/** 缩放模式选项 */ +const ZOOM_MODE_OPTIONS = [ + { value: 'message-area', label: '消息区域' }, + { value: 'global', label: '全局界面' }, +] + /** 根据平台返回缩放快捷键提示 */ const isMac = navigator.userAgent.includes('Mac') const ZOOM_HINT = isMac @@ -31,6 +38,12 @@ const ZOOM_HINT = isMac export function AppearanceSettings(): React.ReactElement { const [themeMode, setThemeMode] = useAtom(themeModeAtom) + const [zoomMode, setZoomMode] = useAtom(zoomModeAtom) + const messageAreaZoomLevel = useAtomValue(messageAreaZoomLevelAtom) + const globalZoomLevel = useAtomValue(globalZoomLevelAtom) + + // 根据当前模式选择显示的缩放级别 + const currentZoomLevel = zoomMode === 'message-area' ? messageAreaZoomLevel : globalZoomLevel /** 切换主题模式 */ const handleThemeChange = React.useCallback((value: string) => { @@ -39,6 +52,13 @@ export function AppearanceSettings(): React.ReactElement { updateThemeMode(mode) }, [setThemeMode]) + /** 切换缩放模式 */ + const handleZoomModeChange = React.useCallback((value: string) => { + const mode = value as ZoomMode + setZoomMode(mode) + updateZoomMode(mode) + }, [setZoomMode]) + return ( - +
+ + + 当前缩放级别: {Math.round(currentZoomLevel * 100)}% · {ZOOM_HINT} + + } + className="-mt-2" + /> +
) diff --git a/apps/electron/src/renderer/components/settings/primitives/SettingsRow.tsx b/apps/electron/src/renderer/components/settings/primitives/SettingsRow.tsx index 018cc29..8a9809d 100644 --- a/apps/electron/src/renderer/components/settings/primitives/SettingsRow.tsx +++ b/apps/electron/src/renderer/components/settings/primitives/SettingsRow.tsx @@ -15,7 +15,7 @@ interface SettingsRowProps { /** 标签左侧图标(可选) */ icon?: React.ReactNode /** 行描述(可选) */ - description?: string + description?: React.ReactNode /** 右侧控件 */ children?: React.ReactNode /** 额外 className */ diff --git a/apps/electron/src/renderer/components/ui/context-menu.tsx b/apps/electron/src/renderer/components/ui/context-menu.tsx index 0cd4b2c..9b03d6f 100644 --- a/apps/electron/src/renderer/components/ui/context-menu.tsx +++ b/apps/electron/src/renderer/components/ui/context-menu.tsx @@ -44,7 +44,7 @@ const ContextMenuSubContent = React.forwardRef< { + initializeZoom(setZoomMode, setMessageAreaZoomLevel, setGlobalZoomLevel).catch(console.error) + }, [setZoomMode, setMessageAreaZoomLevel, setGlobalZoomLevel]) + + // 监听主进程发送的缩放事件 + useEffect(() => { + const cleanupZoomIn = window.electronAPI.onZoomIn(() => { + if (zoomMode === 'message-area') { + const newLevel = zoomIn(messageAreaZoomLevel) + setMessageAreaZoomLevel(newLevel) + updateMessageAreaZoomLevel(newLevel).catch(console.error) + } else { + const newLevel = zoomIn(globalZoomLevel) + setGlobalZoomLevel(newLevel) + updateGlobalZoomLevel(newLevel).catch(console.error) + } + }) + + const cleanupZoomOut = window.electronAPI.onZoomOut(() => { + if (zoomMode === 'message-area') { + const newLevel = zoomOut(messageAreaZoomLevel) + setMessageAreaZoomLevel(newLevel) + updateMessageAreaZoomLevel(newLevel).catch(console.error) + } else { + const newLevel = zoomOut(globalZoomLevel) + setGlobalZoomLevel(newLevel) + updateGlobalZoomLevel(newLevel).catch(console.error) + } + }) + + const cleanupZoomReset = window.electronAPI.onZoomReset(() => { + const newLevel = resetZoom() + if (zoomMode === 'message-area') { + setMessageAreaZoomLevel(newLevel) + updateMessageAreaZoomLevel(newLevel).catch(console.error) + } else { + setGlobalZoomLevel(newLevel) + updateGlobalZoomLevel(newLevel).catch(console.error) + } + }) + + return () => { + cleanupZoomIn() + cleanupZoomOut() + cleanupZoomReset() + } + }, [zoomMode, messageAreaZoomLevel, globalZoomLevel, setMessageAreaZoomLevel, setGlobalZoomLevel]) + + return null +} + /** * Agent IPC 监听器初始化组件 * @@ -172,6 +248,7 @@ function AgentListenersInitializer(): null { ReactDOM.createRoot(document.getElementById('root')!).render( + diff --git a/apps/electron/src/types/settings.ts b/apps/electron/src/types/settings.ts index 19bb710..28bacbf 100644 --- a/apps/electron/src/types/settings.ts +++ b/apps/electron/src/types/settings.ts @@ -12,10 +12,40 @@ export type ThemeMode = 'light' | 'dark' | 'system' /** 默认主题模式 */ export const DEFAULT_THEME_MODE: ThemeMode = 'dark' +/** 缩放模式 */ +export type ZoomMode = 'message-area' | 'global' + +/** 默认缩放模式 */ +export const DEFAULT_ZOOM_MODE: ZoomMode = 'message-area' + +/** 默认消息区域缩放级别 */ +export const DEFAULT_MESSAGE_AREA_ZOOM_LEVEL = 1.0 + +/** 默认全局缩放级别 */ +export const DEFAULT_GLOBAL_ZOOM_LEVEL = 1.0 + +/** 缩放级别范围(消息区域和全局共用) */ +export const ZOOM_MIN = 0.5 +export const ZOOM_MAX = 2.0 +export const ZOOM_STEP = 0.1 + +/** @deprecated 使用 ZOOM_MIN */ +export const MESSAGE_AREA_ZOOM_MIN = ZOOM_MIN +/** @deprecated 使用 ZOOM_MAX */ +export const MESSAGE_AREA_ZOOM_MAX = ZOOM_MAX +/** @deprecated 使用 ZOOM_STEP */ +export const MESSAGE_AREA_ZOOM_STEP = ZOOM_STEP + /** 应用设置 */ export interface AppSettings { /** 主题模式 */ themeMode: ThemeMode + /** 缩放模式 */ + zoomMode?: ZoomMode + /** 消息区域缩放级别 */ + messageAreaZoomLevel?: number + /** 全局缩放级别 */ + globalZoomLevel?: number /** Agent 默认渠道 ID(仅限 Anthropic 渠道) */ agentChannelId?: string /** Agent 默认模型 ID */ @@ -38,4 +68,6 @@ export const SETTINGS_IPC_CHANNELS = { UPDATE: 'settings:update', GET_SYSTEM_THEME: 'settings:get-system-theme', ON_SYSTEM_THEME_CHANGED: 'settings:system-theme-changed', + SET_ZOOM_FACTOR: 'settings:set-zoom-factor', + GET_ZOOM_FACTOR: 'settings:get-zoom-factor', } as const