cc-haha/desktop/src/hooks/useKeyboardShortcuts.ts
程序员阿江(Relakkes) 4b62d354d6 Unify desktop zoom controls
PR #428 added a General Settings zoom slider after the desktop shortcut work already introduced a native-first app zoom controller. Keeping both paths would create double scaling and stale UI state, so the slider now routes through the existing controller and store state while the app shell no longer applies a second CSS zoom.

Constraint: UI zoom is device-local display state and should not be written into shared user settings.

Rejected: Keep cc-haha-ui-zoom plus AppShell style.zoom | it conflicts with shortcut zoom and multiplies visual scale.

Rejected: Persist zoom through /api/settings/user | it would sync display-specific state across machines.

Confidence: high

Scope-risk: moderate

Directive: Keep app zoom behind desktop/src/lib/appZoom.ts; do not add another storage key or DOM zoom application point without migration and shortcut sync tests.

Tested: cd desktop && bun run test -- --run src/lib/appZoom.test.ts src/hooks/useKeyboardShortcuts.test.tsx src/stores/settingsStore.test.ts src/__tests__/generalSettings.test.tsx src/lib/persistenceMigrations.test.ts src/lib/doctorRepair.test.ts

Tested: bun run check:desktop

Not-tested: Real Windows/Linux desktop runtime smoke.
2026-05-14 17:49:57 +08:00

84 lines
2.8 KiB
TypeScript

import { useEffect, useRef } from 'react'
import { useSessionStore } from '../stores/sessionStore'
import { useChatStore } from '../stores/chatStore'
import { useTabStore } from '../stores/tabStore'
import { useUIStore } from '../stores/uiStore'
import {
getAppZoomKeyboardAction,
nextAppZoomLevel,
} from '../lib/appZoom'
import { useSettingsStore } from '../stores/settingsStore'
export function useKeyboardShortcuts() {
const setActiveSession = useSessionStore((s) => s.setActiveSession)
const setActiveView = useUIStore((s) => s.setActiveView)
const setSidebarOpen = useUIStore((s) => s.setSidebarOpen)
const closeModal = useUIStore((s) => s.closeModal)
const activeModal = useUIStore((s) => s.activeModal)
const stopGeneration = useChatStore((s) => s.stopGeneration)
const activeTabId = useTabStore((s) => s.activeTabId)
const chatState = useChatStore((s) => activeTabId ? s.sessions[activeTabId]?.chatState ?? 'idle' : 'idle')
const uiZoom = useSettingsStore((s) => s.uiZoom)
const setUiZoom = useSettingsStore((s) => s.setUiZoom)
const activeModalRef = useRef(activeModal)
activeModalRef.current = activeModal
const chatStateRef = useRef(chatState)
chatStateRef.current = chatState
const activeTabIdRef = useRef(activeTabId)
activeTabIdRef.current = activeTabId
const appZoomLevelRef = useRef(uiZoom)
appZoomLevelRef.current = uiZoom
useEffect(() => {
const handler = (e: KeyboardEvent) => {
const zoomAction = getAppZoomKeyboardAction(e)
if (zoomAction) {
e.preventDefault()
const nextZoom = nextAppZoomLevel(appZoomLevelRef.current, zoomAction)
appZoomLevelRef.current = nextZoom
setUiZoom(nextZoom)
return
}
const meta = e.metaKey || e.ctrlKey
// Cmd+N — New session
if (meta && e.key === 'n') {
e.preventDefault()
setActiveSession(null)
setActiveView('code')
}
// Cmd+K — Focus search (sidebar search input)
if (meta && e.key === 'k') {
e.preventDefault()
setSidebarOpen(true)
requestAnimationFrame(() => {
const searchInput = document.querySelector('#sidebar-search') as HTMLInputElement | null
searchInput?.focus()
searchInput?.select()
})
}
// Escape — Close modal or clear state
if (e.key === 'Escape') {
if (activeModalRef.current) {
closeModal()
}
}
// Cmd+. — Stop generation
if (meta && e.key === '.') {
if (chatStateRef.current !== 'idle' && activeTabIdRef.current) {
e.preventDefault()
stopGeneration(activeTabIdRef.current)
}
}
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [closeModal, setActiveSession, setActiveView, setSidebarOpen, setUiZoom, stopGeneration])
}