From 4e2dc1ecbe235f76626251892cc25c8487344c9d Mon Sep 17 00:00:00 2001 From: CherryXiao Date: Thu, 26 Feb 2026 20:22:19 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=8C=BA=E5=9F=9F=E5=92=8C=E5=85=A8=E5=B1=80=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E7=9A=84=E7=8B=AC=E7=AB=8B=E7=BC=A9=E6=94=BE=E7=BA=A7?= =?UTF-8?q?=E5=88=AB=E7=AE=A1=E7=90=86=EF=BC=88=E5=9F=BA=E7=A1=80=E7=89=88?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 globalZoomLevel 字段到 AppSettings - 创建独立的 globalZoomLevelAtom 状态管理 - 修改 ZoomInitializer 支持两个独立的缩放级别 - 在 AppearanceSettings 中显示当前模式对应的缩放级别 - 全局模式使用 CSS zoom 样式(App.tsx) Co-Authored-By: Claude Sonnet 4.5 --- .../electron/src/main/lib/settings-service.ts | 8 +- apps/electron/src/renderer/App.tsx | 14 +- .../electron/src/renderer/atoms/zoom-atoms.ts | 194 ++++++++++++++++++ .../settings/AppearanceSettings.tsx | 15 +- apps/electron/src/renderer/main.tsx | 77 +++++++ apps/electron/src/types/settings.ts | 23 +++ 6 files changed, 326 insertions(+), 5 deletions(-) create mode 100644 apps/electron/src/renderer/atoms/zoom-atoms.ts diff --git a/apps/electron/src/main/lib/settings-service.ts b/apps/electron/src/main/lib/settings-service.ts index cd96d33..2385615 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, diff --git a/apps/electron/src/renderer/App.tsx b/apps/electron/src/renderer/App.tsx index c6d3117..6ad9222 100644 --- a/apps/electron/src/renderer/App.tsx +++ b/apps/electron/src/renderer/App.tsx @@ -1,9 +1,10 @@ import * as React from 'react' -import { useSetAtom } from 'jotai' +import { useSetAtom, useAtomValue } from 'jotai' import { AppShell } from './components/app-shell/AppShell' import { OnboardingView } from './components/onboarding/OnboardingView' import { TooltipProvider } from './components/ui/tooltip' import { environmentCheckResultAtom } from './atoms/environment' +import { zoomModeAtom, globalZoomLevelAtom } from './atoms/zoom-atoms' import type { AppShellContextType } from './contexts/AppShellContext' export default function App(): React.ReactElement { @@ -11,6 +12,15 @@ export default function App(): React.ReactElement { const [isLoading, setIsLoading] = React.useState(true) const [showOnboarding, setShowOnboarding] = React.useState(false) + // 读取缩放设置 + const zoomMode = useAtomValue(zoomModeAtom) + const globalZoomLevel = useAtomValue(globalZoomLevelAtom) + + // 计算全局缩放样式(仅在全局模式下应用) + const globalZoomStyle = zoomMode === 'global' + ? { zoom: globalZoomLevel, transition: 'zoom 0.2s ease-out' } + : {} + // 初始化:检查 onboarding 状态和环境 React.useEffect(() => { const initialize = async () => { @@ -67,7 +77,7 @@ export default function App(): React.ReactElement { // 显示主界面 return ( - + ) 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..cdc27f1 --- /dev/null +++ b/apps/electron/src/renderer/atoms/zoom-atoms.ts @@ -0,0 +1,194 @@ +/** + * 缩放状态原子 + * + * 管理应用缩放模式、消息区域缩放级别和全局缩放级别。 + * - 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) +} + +/** + * 更新缩放模式并持久化 + */ +export async function updateZoomMode(mode: ZoomMode): Promise { + cacheZoomMode(mode) + await window.electronAPI.updateSettings({ zoomMode: mode }) +} + +/** + * 更新消息区域缩放级别并持久化 + */ +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 }) +} + +/** + * 更新全局缩放级别并持久化 + */ +export async function updateGlobalZoomLevel(level: number): Promise { + // 确保级别在有效范围内 + const clampedLevel = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, level)) + cacheGlobalZoomLevel(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/settings/AppearanceSettings.tsx b/apps/electron/src/renderer/components/settings/AppearanceSettings.tsx index cf7ade8..e83fefb 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,6 +14,7 @@ import { SettingsSegmentedControl, } from './primitives' import { themeModeAtom, updateThemeMode } from '@/atoms/theme' +import { zoomModeAtom, messageAreaZoomLevelAtom, globalZoomLevelAtom } from '@/atoms/zoom-atoms' import type { ThemeMode } from '../../../types' /** 主题选项 */ @@ -31,6 +32,12 @@ const ZOOM_HINT = isMac export function AppearanceSettings(): React.ReactElement { const [themeMode, setThemeMode] = useAtom(themeModeAtom) + const zoomMode = useAtomValue(zoomModeAtom) + const messageAreaZoomLevel = useAtomValue(messageAreaZoomLevelAtom) + const globalZoomLevel = useAtomValue(globalZoomLevelAtom) + + // 根据当前模式显示对应的缩放级别 + const currentZoomLevel = zoomMode === 'message-area' ? messageAreaZoomLevel : globalZoomLevel /** 切换主题模式 */ const handleThemeChange = React.useCallback((value: string) => { @@ -54,7 +61,11 @@ export function AppearanceSettings(): React.ReactElement { /> + 当前缩放级别: {Math.round(currentZoomLevel * 100)}% · {ZOOM_HINT} + + } /> diff --git a/apps/electron/src/renderer/main.tsx b/apps/electron/src/renderer/main.tsx index 54d520a..7e1c244 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 { + zoomModeAtom, + messageAreaZoomLevelAtom, + globalZoomLevelAtom, + initializeZoom, + updateMessageAreaZoomLevel, + updateGlobalZoomLevel, + zoomIn, + zoomOut, + resetZoom, +} from './atoms/zoom-atoms' import { useGlobalAgentListeners } from './hooks/useGlobalAgentListeners' import { Toaster } from './components/ui/sonner' import { UpdateDialog } from './components/settings/UpdateDialog' @@ -158,6 +169,71 @@ function NotificationsInitializer(): null { return null } +/** + * 缩放初始化组件 + * + * 从主进程加载缩放设置并监听缩放事件。 + */ +function ZoomInitializer(): null { + const setZoomMode = useSetAtom(zoomModeAtom) + const setMessageAreaZoomLevel = useSetAtom(messageAreaZoomLevelAtom) + const setGlobalZoomLevel = useSetAtom(globalZoomLevelAtom) + const zoomMode = useAtomValue(zoomModeAtom) + const messageAreaZoomLevel = useAtomValue(messageAreaZoomLevelAtom) + const globalZoomLevel = useAtomValue(globalZoomLevelAtom) + + // 初始化:从主进程加载设置 + useEffect(() => { + 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..263e341 100644 --- a/apps/electron/src/types/settings.ts +++ b/apps/electron/src/types/settings.ts @@ -12,10 +12,33 @@ 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 + /** 应用设置 */ export interface AppSettings { /** 主题模式 */ themeMode: ThemeMode + /** 缩放模式 */ + zoomMode?: ZoomMode + /** 消息区域缩放级别 */ + messageAreaZoomLevel?: number + /** 全局缩放级别 */ + globalZoomLevel?: number /** Agent 默认渠道 ID(仅限 Anthropic 渠道) */ agentChannelId?: string /** Agent 默认模型 ID */ From f97143c0c29515c156907314bf50b6fa8850b511 Mon Sep 17 00:00:00 2001 From: CherryXiao Date: Thu, 26 Feb 2026 20:25:23 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8D=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E7=95=8C=E9=9D=A2=E4=BD=BF=E7=94=A8=20Electron=20?= =?UTF-8?q?=E5=8E=9F=E7=94=9F=E7=BC=A9=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 SET_ZOOM_FACTOR 和 GET_ZOOM_FACTOR IPC 通道 - 在主进程中实现 webContents.setZoomFactor 处理器 - 在 preload 中暴露 setZoomFactor 和 getZoomFactor API - 修改 zoom-atoms.ts 使用 Electron 原生缩放 - 从 App.tsx 移除全局模式的 CSS zoom 样式 使用 Electron 原生缩放避免了滚动条问题,提供更好的用户体验。 Co-Authored-By: Claude Sonnet 4.5 --- apps/electron/src/main/ipc.ts | 23 ++++++++++++++++ apps/electron/src/preload/index.ts | 14 ++++++++++ apps/electron/src/renderer/App.tsx | 14 ++-------- .../electron/src/renderer/atoms/zoom-atoms.ts | 26 +++++++++++++++++++ apps/electron/src/types/settings.ts | 2 ++ 5 files changed, 67 insertions(+), 12 deletions(-) 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/preload/index.ts b/apps/electron/src/preload/index.ts index 20c563f..31a0517 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 + // ===== 环境检测相关 ===== /** 执行环境检测 */ @@ -590,6 +596,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) diff --git a/apps/electron/src/renderer/App.tsx b/apps/electron/src/renderer/App.tsx index 6ad9222..c6d3117 100644 --- a/apps/electron/src/renderer/App.tsx +++ b/apps/electron/src/renderer/App.tsx @@ -1,10 +1,9 @@ import * as React from 'react' -import { useSetAtom, useAtomValue } from 'jotai' +import { useSetAtom } from 'jotai' import { AppShell } from './components/app-shell/AppShell' import { OnboardingView } from './components/onboarding/OnboardingView' import { TooltipProvider } from './components/ui/tooltip' import { environmentCheckResultAtom } from './atoms/environment' -import { zoomModeAtom, globalZoomLevelAtom } from './atoms/zoom-atoms' import type { AppShellContextType } from './contexts/AppShellContext' export default function App(): React.ReactElement { @@ -12,15 +11,6 @@ export default function App(): React.ReactElement { const [isLoading, setIsLoading] = React.useState(true) const [showOnboarding, setShowOnboarding] = React.useState(false) - // 读取缩放设置 - const zoomMode = useAtomValue(zoomModeAtom) - const globalZoomLevel = useAtomValue(globalZoomLevelAtom) - - // 计算全局缩放样式(仅在全局模式下应用) - const globalZoomStyle = zoomMode === 'global' - ? { zoom: globalZoomLevel, transition: 'zoom 0.2s ease-out' } - : {} - // 初始化:检查 onboarding 状态和环境 React.useEffect(() => { const initialize = async () => { @@ -77,7 +67,7 @@ export default function App(): React.ReactElement { // 显示主界面 return ( - + ) diff --git a/apps/electron/src/renderer/atoms/zoom-atoms.ts b/apps/electron/src/renderer/atoms/zoom-atoms.ts index cdc27f1..38e5ed4 100644 --- a/apps/electron/src/renderer/atoms/zoom-atoms.ts +++ b/apps/electron/src/renderer/atoms/zoom-atoms.ts @@ -140,14 +140,33 @@ export async function initializeZoom( 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) + } } /** @@ -162,11 +181,18 @@ export async function updateMessageAreaZoomLevel(level: number): Promise { /** * 更新全局缩放级别并持久化 + * + * 使用 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 }) } diff --git a/apps/electron/src/types/settings.ts b/apps/electron/src/types/settings.ts index 263e341..544429b 100644 --- a/apps/electron/src/types/settings.ts +++ b/apps/electron/src/types/settings.ts @@ -61,4 +61,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 From 66df4d60f5f0af19cc3877c601320a02293ac0c1 Mon Sep 17 00:00:00 2001 From: CherryXiao Date: Thu, 26 Feb 2026 20:30:04 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8D=E5=B9=B6?= =?UTF-8?q?=E6=8E=92=E6=A8=A1=E5=BC=8F=E7=9A=84=E7=BC=A9=E6=94=BE=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 ParallelChatMessages 组件中应用消息区域缩放 - 支持简单布局和分段布局两种模式的缩放 - 确保并排模式下缩放与普通模式保持一致 Co-Authored-By: Claude Sonnet 4.5 --- .../components/chat/ParallelChatMessages.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/electron/src/renderer/components/chat/ParallelChatMessages.tsx b/apps/electron/src/renderer/components/chat/ParallelChatMessages.tsx index 0527f81..d588e8f 100644 --- a/apps/electron/src/renderer/components/chat/ParallelChatMessages.tsx +++ b/apps/electron/src/renderer/components/chat/ParallelChatMessages.tsx @@ -29,6 +29,7 @@ import { ReasoningContent, } from '@/components/ai-elements/reasoning' import { streamingModelAtom } from '@/atoms/chat-atoms' +import { zoomModeAtom, messageAreaZoomLevelAtom } from '@/atoms/zoom-atoms' import { getModelLogo } from '@/lib/model-logo' import type { ChatMessage } from '@proma/shared' @@ -242,6 +243,13 @@ export function ParallelChatMessages({ inlineEditingMessageId, loadingMore = false, }: ParallelChatMessagesProps): React.ReactElement { + // 读取缩放设置 + const zoomMode = useAtomValue(zoomModeAtom) + const zoomLevel = useAtomValue(messageAreaZoomLevelAtom) + + // 计算缩放样式(仅在消息区域模式下应用) + const zoomStyle = zoomMode === 'message-area' ? { zoom: zoomLevel, transition: 'zoom 0.2s ease-out' } : {} + // 分段消息 const segments = useMemo( () => 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 && } From 1408dea21471dd1d417d0845fbbd2d98a89ade47 Mon Sep 17 00:00:00 2001 From: CherryXiao Date: Thu, 26 Feb 2026 20:57:28 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=E7=BC=A9=E6=94=BE=E7=BA=A7=E5=88=AB=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加消息区域和全局界面两种缩放模式 - 消息区域模式:仅缩放聊天内容区域(使用 CSS zoom) - 全局界面模式:缩放整个应用窗口(使用 Electron setZoomFactor) - 支持快捷键:Cmd/Ctrl + Plus(放大)、Cmd/Ctrl + -(缩小)、Cmd/Ctrl + 0(重置) - 在设置页面添加缩放模式切换和当前缩放级别显示 - 支持 Chat 模式(普通/并排)和 Agent 模式的独立缩放 - 使用自定义 IPC 事件替代 Electron 内置缩放角色,实现更灵活的缩放控制 - 缩放设置持久化到 ~/.proma/settings.json 并使用 localStorage 缓存 --- .../electron/src/main/lib/settings-service.ts | 2 + apps/electron/src/main/menu.ts | 33 +++++++++++-- apps/electron/src/preload/index.ts | 31 ++++++++++++ apps/electron/src/renderer/atoms/index.ts | 1 + .../components/agent/AgentMessages.tsx | 8 +++- .../renderer/components/chat/ChatMessages.tsx | 11 ++++- .../settings/AppearanceSettings.tsx | 47 ++++++++++++++----- .../settings/primitives/SettingsRow.tsx | 2 +- apps/electron/src/renderer/main.tsx | 24 +++++----- apps/electron/src/types/settings.ts | 9 +++- 10 files changed, 136 insertions(+), 32 deletions(-) diff --git a/apps/electron/src/main/lib/settings-service.ts b/apps/electron/src/main/lib/settings-service.ts index 2385615..b9a3bbf 100644 --- a/apps/electron/src/main/lib/settings-service.ts +++ b/apps/electron/src/main/lib/settings-service.ts @@ -50,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 31a0517..0ec5022 100644 --- a/apps/electron/src/preload/index.ts +++ b/apps/electron/src/preload/index.ts @@ -427,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 } /** @@ -889,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/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 ( - + {/* 滚动到顶部时自动加载更多历史 */} { + const mode = value as ZoomMode + setZoomMode(mode) + updateZoomMode(mode) + }, [setZoomMode]) + return ( - - 当前缩放级别: {Math.round(currentZoomLevel * 100)}% · {ZOOM_HINT} - - } - /> +
+ + + 当前缩放级别: {Math.round(currentZoomLevel * 100)}% · {ZOOM_HINT} + + } + className="-mt-4" + /> +
) 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/main.tsx b/apps/electron/src/renderer/main.tsx index 7e1c244..9c57c80 100644 --- a/apps/electron/src/renderer/main.tsx +++ b/apps/electron/src/renderer/main.tsx @@ -15,6 +15,17 @@ import { applyThemeToDOM, initializeTheme, } from './atoms/theme' +import { + zoomModeAtom, + messageAreaZoomLevelAtom, + globalZoomLevelAtom, + initializeZoom, + updateMessageAreaZoomLevel, + updateGlobalZoomLevel, + zoomIn, + zoomOut, + resetZoom, +} from './atoms/zoom-atoms' import { agentChannelIdAtom, agentModelIdAtom, @@ -28,17 +39,6 @@ import { notificationsEnabledAtom, initializeNotifications, } from './atoms/notifications' -import { - zoomModeAtom, - messageAreaZoomLevelAtom, - globalZoomLevelAtom, - initializeZoom, - updateMessageAreaZoomLevel, - updateGlobalZoomLevel, - zoomIn, - zoomOut, - resetZoom, -} from './atoms/zoom-atoms' import { useGlobalAgentListeners } from './hooks/useGlobalAgentListeners' import { Toaster } from './components/ui/sonner' import { UpdateDialog } from './components/settings/UpdateDialog' @@ -172,7 +172,7 @@ function NotificationsInitializer(): null { /** * 缩放初始化组件 * - * 从主进程加载缩放设置并监听缩放事件。 + * 从主进程加载缩放设置,监听缩放事件。 */ function ZoomInitializer(): null { const setZoomMode = useSetAtom(zoomModeAtom) diff --git a/apps/electron/src/types/settings.ts b/apps/electron/src/types/settings.ts index 544429b..28bacbf 100644 --- a/apps/electron/src/types/settings.ts +++ b/apps/electron/src/types/settings.ts @@ -24,11 +24,18 @@ 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 { /** 主题模式 */