diff --git a/packages/shared/data/preferences.ts b/packages/shared/data/preferences.ts index d9107bd706..99e5ecbbed 100644 --- a/packages/shared/data/preferences.ts +++ b/packages/shared/data/preferences.ts @@ -1,3 +1,5 @@ +import type { SelectionActionItem, SelectionFilterMode, SelectionTriggerMode } from './types' + /** * Auto-generated preferences configuration * Generated at: 2025-08-15T03:23:46.568Z @@ -13,7 +15,6 @@ "interfaces": { "order": "alphabetically" }, "typeLiterals": { "order": "alphabetically" } }] */ - export interface PreferencesType { default: { // redux/settings/enableDeveloperMode @@ -289,7 +290,7 @@ export interface PreferencesType { // redux/settings/readClipboardAtStartup 'feature.quick_assistant.read_clipboard_at_startup': boolean // redux/selectionStore/actionItems - 'feature.selection.action_items': unknown[] + 'feature.selection.action_items': SelectionActionItem[] // redux/selectionStore/actionWindowOpacity 'feature.selection.action_window_opacity': number // redux/selectionStore/isAutoClose @@ -303,13 +304,13 @@ export interface PreferencesType { // redux/selectionStore/filterList 'feature.selection.filter_list': string[] // redux/selectionStore/filterMode - 'feature.selection.filter_mode': string + 'feature.selection.filter_mode': SelectionFilterMode // redux/selectionStore/isFollowToolbar 'feature.selection.follow_toolbar': boolean // redux/selectionStore/isRemeberWinSize 'feature.selection.remember_win_size': boolean // redux/selectionStore/triggerMode - 'feature.selection.trigger_mode': string + 'feature.selection.trigger_mode': SelectionTriggerMode // redux/shortcuts/shortcuts.clear_topic 'shortcut.chat.clear': Record // redux/shortcuts/shortcuts.copy_last_message diff --git a/packages/shared/data/types.d.ts b/packages/shared/data/types.d.ts index a334a11d84..5e46a39222 100644 --- a/packages/shared/data/types.d.ts +++ b/packages/shared/data/types.d.ts @@ -3,6 +3,10 @@ import { PreferencesType } from './preferences' export type PreferenceDefaultScopeType = PreferencesType['default'] export type PreferenceKeyType = keyof PreferenceDefaultScopeType +export interface PreferenceUpdateOptions { + optimistic: boolean +} + export type PreferenceShortcutType = { key: string[] editable: boolean @@ -10,6 +14,16 @@ export type PreferenceShortcutType = { system: boolean } -export interface PreferenceUpdateOptions { - optimistic: boolean +export type SelectionTriggerMode = 'selected' | 'ctrlkey' | 'shortcut' +export type SelectionFilterMode = 'default' | 'whitelist' | 'blacklist' +export interface SelectionActionItem { + id: string + name: string + enabled: boolean + isBuiltIn: boolean + icon?: string + prompt?: string + assistantId?: string + selectedText?: string + searchEngine?: string } diff --git a/src/main/index.ts b/src/main/index.ts index 3900a61971..05b9f13242 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -159,16 +159,16 @@ if (!app.requestSingleInstanceLock()) { await preferenceService.initialize() - // Create two test windows for cross-window preference sync testing - logger.info('Creating test windows for PreferenceService cross-window sync testing') - const testWindow1 = dataRefactorMigrateService.createTestWindow() - const testWindow2 = dataRefactorMigrateService.createTestWindow() + // // Create two test windows for cross-window preference sync testing + // logger.info('Creating test windows for PreferenceService cross-window sync testing') + // const testWindow1 = dataRefactorMigrateService.createTestWindow() + // const testWindow2 = dataRefactorMigrateService.createTestWindow() - // Position windows to avoid overlap - testWindow1.once('ready-to-show', () => { - const [x, y] = testWindow1.getPosition() - testWindow2.setPosition(x + 50, y + 50) - }) + // // Position windows to avoid overlap + // testWindow1.once('ready-to-show', () => { + // const [x, y] = testWindow1.getPosition() + // testWindow2.setPosition(x + 50, y + 50) + // }) /************FOR TESTING ONLY END****************/ // Set app user model id for windows diff --git a/src/main/services/SelectionService.ts b/src/main/services/SelectionService.ts index 5a9803c458..70c35e7add 100644 --- a/src/main/services/SelectionService.ts +++ b/src/main/services/SelectionService.ts @@ -2,6 +2,7 @@ import { preferenceService } from '@data/PreferenceService' import { loggerService } from '@logger' import { SELECTION_FINETUNED_LIST, SELECTION_PREDEFINED_BLACKLIST } from '@main/configs/SelectionConfig' import { isDev, isMac, isWin } from '@main/constant' +import type { SelectionActionItem, SelectionFilterMode, SelectionTriggerMode } from '@shared/data/types' import { IpcChannel } from '@shared/IpcChannel' import { app, BrowserWindow, ipcMain, screen, systemPreferences } from 'electron' import { join } from 'path' @@ -13,8 +14,6 @@ import type { TextSelectionData } from 'selection-hook' -import type { ActionItem } from '../../renderer/src/types/selectionTypes' - const logger = loggerService.withContext('SelectionService') const isSupportedOS = isWin || isMac @@ -1248,7 +1247,7 @@ export class SelectionService { * @param actionItem Action item to process * @param isFullScreen [macOS] only macOS has the available isFullscreen mode */ - public processAction(actionItem: ActionItem, isFullScreen: boolean = false): void { + public processAction(actionItem: SelectionActionItem, isFullScreen: boolean = false): void { const actionWindow = this.popActionWindow() actionWindow.webContents.send(IpcChannel.Selection_UpdateActionData, actionItem) @@ -1476,7 +1475,7 @@ export class SelectionService { preferenceService.set('feature.selection.enabled', enabled) }) - ipcMain.handle(IpcChannel.Selection_SetTriggerMode, (_, triggerMode: string) => { + ipcMain.handle(IpcChannel.Selection_SetTriggerMode, (_, triggerMode: SelectionTriggerMode) => { preferenceService.set('feature.selection.trigger_mode', triggerMode) }) @@ -1488,7 +1487,7 @@ export class SelectionService { preferenceService.set('feature.selection.remember_win_size', isRemeberWinSize) }) - ipcMain.handle(IpcChannel.Selection_SetFilterMode, (_, filterMode: string) => { + ipcMain.handle(IpcChannel.Selection_SetFilterMode, (_, filterMode: SelectionFilterMode) => { preferenceService.set('feature.selection.filter_mode', filterMode) }) @@ -1497,9 +1496,12 @@ export class SelectionService { }) // [macOS] only macOS has the available isFullscreen mode - ipcMain.handle(IpcChannel.Selection_ProcessAction, (_, actionItem: ActionItem, isFullScreen: boolean = false) => { - selectionService?.processAction(actionItem, isFullScreen) - }) + ipcMain.handle( + IpcChannel.Selection_ProcessAction, + (_, actionItem: SelectionActionItem, isFullScreen: boolean = false) => { + selectionService?.processAction(actionItem, isFullScreen) + } + ) ipcMain.handle(IpcChannel.Selection_ActionWindowClose, (event) => { const actionWindow = BrowserWindow.fromWebContents(event.sender) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index ad18a9b193..27df3fda29 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,5 +1,6 @@ import '@renderer/databases' +import { preferenceService } from '@data/PreferenceService' import { loggerService } from '@logger' import store, { persistor } from '@renderer/store' import { Provider } from 'react-redux' @@ -15,6 +16,8 @@ import Router from './Router' const logger = loggerService.withContext('App.tsx') +preferenceService.loadAll() + function App(): React.ReactElement { logger.info('App initialized') diff --git a/src/renderer/src/data/PreferenceService.ts b/src/renderer/src/data/PreferenceService.ts index 2ad2db135a..30213d6452 100644 --- a/src/renderer/src/data/PreferenceService.ts +++ b/src/renderer/src/data/PreferenceService.ts @@ -109,7 +109,7 @@ export class PreferenceService { this.notifyChangeListeners(key) // Auto-subscribe to this key for future updates - await this.subscribeToKeyInternal(key) + await this.subscribeToKeyInternal([key]) return value } catch (error) { @@ -230,7 +230,7 @@ export class PreferenceService { this.notifyChangeListeners(key) - await this.subscribeToKeyInternal(key as PreferenceKeyType) + await this.subscribeToKeyInternal([key as PreferenceKeyType]) } return { ...cachedResults, ...uncachedResults } @@ -343,15 +343,16 @@ export class PreferenceService { /** * Subscribe to a specific key for change notifications */ - private async subscribeToKeyInternal(key: PreferenceKeyType): Promise { - if (this.subscribedKeys.has(key)) return + private async subscribeToKeyInternal(keys: PreferenceKeyType[]): Promise { + const keysToSubscribe = keys.filter((key) => !this.subscribedKeys.has(key)) + if (keysToSubscribe.length === 0) return try { - await window.api.preference.subscribe([key]) - this.subscribedKeys.add(key) - logger.debug(`Subscribed to preference key: ${key}`) + await window.api.preference.subscribe(keysToSubscribe) + keysToSubscribe.forEach((key) => this.subscribedKeys.add(key)) + logger.debug(`Subscribed to preference keys: ${keysToSubscribe.join(', ')}`) } catch (error) { - logger.error(`Failed to subscribe to preference key ${key}:`, error as Error) + logger.error(`Failed to subscribe to preference keys ${keysToSubscribe.join(', ')}:`, error as Error) } } @@ -379,7 +380,7 @@ export class PreferenceService { keyListeners.add(callback) // Auto-subscribe to this key for updates - this.subscribeToKeyInternal(key) + this.subscribeToKeyInternal([key]) return () => { keyListeners.delete(callback) @@ -417,11 +418,10 @@ export class PreferenceService { // Notify change listeners for the loaded value this.notifyChangeListeners(key) - - // Auto-subscribe to this key if not already subscribed - await this.subscribeToKeyInternal(key as PreferenceKeyType) } + await this.subscribeToKeyInternal(Object.keys(allPreferences) as PreferenceKeyType[]) + this.fullCacheLoaded = true logger.info(`Loaded all ${Object.keys(allPreferences).length} preferences into cache`) diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx b/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx index d9693f028b..06303e52eb 100644 --- a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx +++ b/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx @@ -1,11 +1,9 @@ import { usePreference } from '@data/hooks/usePreference' -import { loggerService } from '@logger' import { isMac, isWin } from '@renderer/config/constant' import { useTheme } from '@renderer/context/ThemeProvider' import { getSelectionDescriptionLabel } from '@renderer/i18n/label' -import { FilterMode, TriggerMode } from '@renderer/types/selectionTypes' -import { ActionItem } from '@renderer/types/selectionTypes' import SelectionToolbar from '@renderer/windows/selection/toolbar/SelectionToolbar' +import type { SelectionFilterMode, SelectionTriggerMode } from '@shared/data/types' import { Button, Radio, Row, Slider, Switch, Tooltip } from 'antd' import { CircleHelp, Edit2 } from 'lucide-react' import { FC, useEffect, useState } from 'react' @@ -26,35 +24,9 @@ import MacProcessTrustHintModal from './components/MacProcessTrustHintModal' import SelectionActionsList from './components/SelectionActionsList' import SelectionFilterListModal from './components/SelectionFilterListModal' -const logger = loggerService.withContext('Settings:SelectionAssistant') - const SelectionAssistantSettings: FC = () => { const { theme } = useTheme() const { t } = useTranslation() - // const { - // selectionEnabled, - // triggerMode, - // isCompact, - // isAutoClose, - // isAutoPin, - // isFollowToolbar, - // isRemeberWinSize, - // actionItems, - // actionWindowOpacity, - // filterMode, - // filterList, - // setSelectionEnabled, - // setTriggerMode, - // setIsCompact, - // setIsAutoClose, - // setIsAutoPin, - // setIsFollowToolbar, - // setIsRemeberWinSize, - // setActionWindowOpacity, - // setActionItems, - // setFilterMode, - // setFilterList - // } = useSelectionAssistant() const [selectionEnabled, setSelectionEnabled] = usePreference('feature.selection.enabled') const [triggerMode, setTriggerMode] = usePreference('feature.selection.trigger_mode') @@ -68,18 +40,6 @@ const SelectionAssistantSettings: FC = () => { const [filterList, setFilterList] = usePreference('feature.selection.filter_list') const [actionItems, setActionItems] = usePreference('feature.selection.action_items') - logger.debug(`selectionEnabled: ${selectionEnabled}`) - logger.debug(`triggerMode: ${triggerMode}`) - logger.debug(`isCompact: ${isCompact}`) - logger.debug(`isAutoClose: ${isAutoClose}`) - logger.debug(`isAutoPin: ${isAutoPin}`) - logger.debug(`isFollowToolbar: ${isFollowToolbar}`) - logger.debug(`isRemeberWinSize: ${isRemeberWinSize}`) - logger.debug(`actionWindowOpacity: ${actionWindowOpacity}`) - logger.debug(`filterMode: ${filterMode}`) - logger.debug(`filterList: ${filterList}`) - logger.debug(`actionItems: ${actionItems}`) - const isSupportedOS = isWin || isMac const [isFilterListModalOpen, setIsFilterListModalOpen] = useState(false) @@ -168,7 +128,7 @@ const SelectionAssistantSettings: FC = () => { setTriggerMode(e.target.value as TriggerMode)} + onChange={(e) => setTriggerMode(e.target.value as SelectionTriggerMode)} buttonStyle="solid"> {t('selection.settings.toolbar.trigger_mode.selected')} @@ -257,7 +217,7 @@ const SelectionAssistantSettings: FC = () => { - + {t('selection.settings.advanced.title')} @@ -269,7 +229,7 @@ const SelectionAssistantSettings: FC = () => { setFilterMode(e.target.value as FilterMode)} + onChange={(e) => setFilterMode(e.target.value as SelectionFilterMode)} buttonStyle="solid"> {t('selection.settings.advanced.filter_mode.default')} {t('selection.settings.advanced.filter_mode.whitelist')} diff --git a/src/renderer/src/types/selectionTypes.d.ts b/src/renderer/src/types/selectionTypes.d.ts index 84b532afaa..e69de29bb2 100644 --- a/src/renderer/src/types/selectionTypes.d.ts +++ b/src/renderer/src/types/selectionTypes.d.ts @@ -1,27 +0,0 @@ -export type TriggerMode = 'selected' | 'ctrlkey' | 'shortcut' -export type FilterMode = 'default' | 'whitelist' | 'blacklist' -export interface ActionItem { - id: string - name: string - enabled: boolean - isBuiltIn: boolean - icon?: string - prompt?: string - assistantId?: string - selectedText?: string - searchEngine?: string -} - -export interface SelectionState { - selectionEnabled: boolean - triggerMode: TriggerMode - isCompact: boolean - isAutoClose: boolean - isAutoPin: boolean - isFollowToolbar: boolean - isRemeberWinSize: boolean - filterMode: FilterMode - filterList: string[] - actionWindowOpacity: number - actionItems: ActionItem[] -} diff --git a/src/renderer/src/windows/selection/action/SelectionActionApp.tsx b/src/renderer/src/windows/selection/action/SelectionActionApp.tsx index 5112caf945..4d45fc26f5 100644 --- a/src/renderer/src/windows/selection/action/SelectionActionApp.tsx +++ b/src/renderer/src/windows/selection/action/SelectionActionApp.tsx @@ -1,9 +1,9 @@ +import { usePreference } from '@data/hooks/usePreference' import { isMac } from '@renderer/config/constant' -import { useSelectionAssistant } from '@renderer/hooks/useSelectionAssistant' import { useSettings } from '@renderer/hooks/useSettings' import i18n from '@renderer/i18n' -import type { ActionItem } from '@renderer/types/selectionTypes' import { defaultLanguage } from '@shared/config/constant' +import type { SelectionActionItem } from '@shared/data/types' import { IpcChannel } from '@shared/IpcChannel' import { Button, Slider, Tooltip } from 'antd' import { Droplet, Minus, Pin, X } from 'lucide-react' @@ -20,10 +20,13 @@ const SelectionActionApp: FC = () => { const { t } = useTranslation() - const [action, setAction] = useState(null) + const [action, setAction] = useState(null) const isActionLoaded = useRef(false) - const { isAutoClose, isAutoPin, actionWindowOpacity } = useSelectionAssistant() + const [isAutoClose] = usePreference('feature.selection.auto_close') + const [isAutoPin] = usePreference('feature.selection.auto_pin') + const [actionWindowOpacity] = usePreference('feature.selection.action_window_opacity') + const [isPinned, setIsPinned] = useState(isAutoPin) const [isWindowFocus, setIsWindowFocus] = useState(true) @@ -38,7 +41,7 @@ const SelectionActionApp: FC = () => { useEffect(() => { const actionListenRemover = window.electron?.ipcRenderer.on( IpcChannel.Selection_UpdateActionData, - (_, actionItem: ActionItem) => { + (_, actionItem: SelectionActionItem) => { setAction(actionItem) isActionLoaded.current = true } diff --git a/src/renderer/src/windows/selection/action/components/ActionGeneral.tsx b/src/renderer/src/windows/selection/action/components/ActionGeneral.tsx index 9f3762b15f..e501696d6e 100644 --- a/src/renderer/src/windows/selection/action/components/ActionGeneral.tsx +++ b/src/renderer/src/windows/selection/action/components/ActionGeneral.tsx @@ -11,8 +11,8 @@ import { } from '@renderer/services/AssistantService' import { pauseTrace } from '@renderer/services/SpanManagerService' import { Assistant, Topic } from '@renderer/types' -import type { ActionItem } from '@renderer/types/selectionTypes' import { abortCompletion } from '@renderer/utils/abortController' +import type { SelectionActionItem } from '@shared/data/types' import { ChevronDown } from 'lucide-react' import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -22,7 +22,7 @@ import { processMessages } from './ActionUtils' import WindowFooter from './WindowFooter' interface Props { - action: ActionItem + action: SelectionActionItem scrollToBottom?: () => void } diff --git a/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx b/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx index 6a40498cb4..f06522a638 100644 --- a/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx +++ b/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx @@ -1,12 +1,12 @@ import '@renderer/assets/styles/selection-toolbar.scss' +import { usePreference } from '@data/hooks/usePreference' import { loggerService } from '@logger' import { AppLogo } from '@renderer/config/env' -import { useSelectionAssistant } from '@renderer/hooks/useSelectionAssistant' import { useSettings } from '@renderer/hooks/useSettings' import i18n from '@renderer/i18n' -import type { ActionItem } from '@renderer/types/selectionTypes' import { defaultLanguage } from '@shared/config/constant' +import type { SelectionActionItem } from '@shared/data/types' import { IpcChannel } from '@shared/IpcChannel' import { Avatar } from 'antd' import { ClipboardCheck, ClipboardCopy, ClipboardX, MessageSquareHeart } from 'lucide-react' @@ -32,9 +32,9 @@ const updateWindowSize = () => { * ActionIcons is a component that renders the action icons */ const ActionIcons: FC<{ - actionItems: ActionItem[] + actionItems: SelectionActionItem[] isCompact: boolean - handleAction: (action: ActionItem) => void + handleAction: (action: SelectionActionItem) => void copyIconStatus: 'normal' | 'success' | 'fail' copyIconAnimation: 'none' | 'enter' | 'exit' }> = memo(({ actionItems, isCompact, handleAction, copyIconStatus, copyIconAnimation }) => { @@ -67,7 +67,7 @@ const ActionIcons: FC<{ }, [copyIconStatus, copyIconAnimation]) const renderActionButton = useCallback( - (action: ActionItem) => { + (action: SelectionActionItem) => { const displayName = action.isBuiltIn ? t(action.name) : action.name return ( @@ -99,7 +99,8 @@ const ActionIcons: FC<{ */ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => { const { language, customCss } = useSettings() - const { isCompact, actionItems } = useSelectionAssistant() + const [isCompact] = usePreference('feature.selection.compact') + const [actionItems] = usePreference('feature.selection.action_items') const [animateKey, setAnimateKey] = useState(0) const [copyIconStatus, setCopyIconStatus] = useState<'normal' | 'success' | 'fail'>('normal') const [copyIconAnimation, setCopyIconAnimation] = useState<'none' | 'enter' | 'exit'>('none') @@ -179,7 +180,7 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => { } const handleAction = useCallback( - (action: ActionItem) => { + (action: SelectionActionItem) => { if (demo) return /** avoid mutating the original action, it will cause syncing issue */ @@ -216,7 +217,7 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => { } } - const handleSearch = (action: ActionItem) => { + const handleSearch = (action: SelectionActionItem) => { if (!action.searchEngine) return const customUrl = action.searchEngine.split('|')[1] @@ -230,14 +231,14 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => { /** * Quote the selected text to the inputbar of the main window */ - const handleQuote = (action: ActionItem) => { + const handleQuote = (action: SelectionActionItem) => { if (action.selectedText) { window.api?.quoteToMainWindow(action.selectedText) window.api?.selection.hideToolbar() } } - const handleDefaultAction = (action: ActionItem) => { + const handleDefaultAction = (action: SelectionActionItem) => { // [macOS] only macOS has the available isFullscreen mode window.api?.selection.processAction(action, isFullScreen.current) window.api?.selection.hideToolbar()