cherry-studio/src/preload/index.ts
SuYao a6182eaf85
Refactor/inputbar (#10332)
* Refactor inputbar system with configurable scope-based architecture

- **Implement scope-based configuration** for chat, agent sessions, and mini-window with feature toggles
- **Add tool registry system** with dependency injection for modular inputbar tools
- **Create shared state management** via InputbarToolsProvider for consistent state handling
- **Migrate existing tools** to registry-based definitions with proper scope filtering

The changes introduce a flexible inputbar architecture that supports different use cases through scope-based configuration while maintaining feature parity and improving code organization.

* Remove unused import and refactor tool rendering

- Delete obsolete '@renderer/pages/home/Inputbar/tools' import from Inputbar.tsx
- Extract ToolButton component to render tools outside useMemo dependency cycle
- Store tool definitions in config for deferred rendering with current context
- Fix potential stale closure issues in tool rendering by rebuilding context on each render

* Wrap ToolButton in React.memo and optimize quick panel menu updates

- Memoize ToolButton component to prevent unnecessary re-renders when tool key remains unchanged
- Replace direct menu state updates with version-based triggering to batch registry changes
- Add useEffect to consolidate menu updates and reduce redundant flat operations

* chore style

* refactor(InputbarToolsProvider): simplify quick panel menu update logic

* Improve QuickPanel behavior and input handling

- Default select first item when panel symbol changes to enhance user experience
- Add Tab key support for selecting template variables in input field
- Refactor QuickPanel trigger logic with better symbol tracking and boundary checks
- Fix typo in translation key for model selection menu item

* Refactor import statements to use type-only imports

- Convert inline type imports to explicit type imports in Inputbar.tsx and types.ts
- Replace combined type/value imports with separate type imports in InputbarToolsProvider and tools
- Remove unnecessary menu version state and effect in InputbarToolsProvider

* Refactor InputbarTools context to separate state and dispatch concerns

- Split single context into separate state and dispatch contexts to optimize re-renders
- Introduce derived state for `couldMentionNotVisionModel` based on file types
- Encapsulate Quick Panel API in stable object with memoized functions
- Add internal dispatch context for Inputbar-specific state setters

* Refactor Inputbar to use split context hooks and optimize QuickPanel

- Replace monolithic `useInputbarTools` with separate state, dispatch, and internal dispatch hooks
- Move text state from context to local component state in InputbarInner
- Optimize QuickPanel trigger registration to use ref pattern, avoiding frequent re-registrations

* Refactor QuickPanel API to separate concerns between tools and inputbar

- Split QuickPanel API into `toolsRegistry` for tool registration and `triggers` for inputbar triggering
- Remove unused QuickPanel state variables and clean up dependencies
- Update tool context to use new API structure with proper type safety

* Optimize the state management of QuickPanel and Inputbar, add text update functionality, and improve the tool registration logic.

* chore

* Add reusable React hooks and InputbarCore component for chat input

- Create `useInputText`, `useKeyboardHandler`, and `useTextareaResize` hooks for text management, keyboard shortcuts, and auto-resizing
- Implement `InputbarCore` component with modular toolbar sections, drag-drop support, and textarea customization
- Add `useFileDragDrop` and `usePasteHandler` hooks for file uploads and paste handling with type filtering

* Refactor Inputbar to use custom hooks for text and textarea management

- Replace manual text state with useInputText hook for text management and empty state
- Replace textarea resize logic with useTextareaResize hook for automatic height adjustment
- Add comprehensive refactoring documentation with usage examples and guidelines

* Refactor inputbar drag-drop and paste handling into custom hooks

- Extract paste handling logic into usePasteHandler hook
- Extract drag-drop file handling into useFileDragDrop hook
- Remove inline drag-drop state and handlers, use hook interfaces
- Clean up dependencies and callback optimizations

* Refactor Inputbar component to use InputbarCore composition

- Extract complex UI logic into InputbarCore component for better separation of concerns
- Remove intermediate wrapper component and action ref forwarding pattern
- Consolidate focus/blur handlers and simplify component structure

* Refactor Inputbar to expose actions via ref for external control

- Extract action handlers into ProviderActionHandlers interface and expose via ref
- Split component into Inputbar wrapper and InputbarInner implementation
- Update useEffect to sync inner component actions with ref for external access

* feat: inputbar core

* refactor: Update QuickPanel integration across various tools

* refactor: migrate to antd

* chore: format

* fix: clean code

* clean code

* fix i18n

* fix: i18n

* relative path

* model type

* 🤖 Weekly Automated Update: Nov 09, 2025 (#11209)

feat(bot): Weekly automated script run

Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com>
Co-authored-by: SuYao <sy20010504@gmail.com>

* format

* fix

* fix: format

* use ripgrep

* update with input

* add common filters

* fix build issue

* format

* fix error

* smooth change

* adjust

* support listing dir

* keep list files when focus and blur

* support draft save

* Optimize the rendering logic of session messages and input bars, and simplify conditional judgments.

* Upgrade to agentId

* format

* 🐛 fix: force quick triggers for agent sessions

* revert

* fix migrate

* fix: filter

* fix: trigger

* chore packages

* feat: 添加过滤和排序功能,支持自定义函数

* fix cursor bug

* fix format

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: beyondkmp <beyondkmp@gmail.com>
Co-authored-by: kangfenmao <kangfenmao@qq.com>
2025-11-12 20:04:58 +08:00

592 lines
34 KiB
TypeScript

import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
import { electronAPI } from '@electron-toolkit/preload'
import type { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
import type { SpanContext } from '@opentelemetry/api'
import type { TerminalConfig, UpgradeChannel } from '@shared/config/constant'
import type { LogLevel, LogSourceWithContext } from '@shared/config/logger'
import type { FileChangeEvent, WebviewKeyEvent } from '@shared/config/types'
import { IpcChannel } from '@shared/IpcChannel'
import type { Notification } from '@types'
import type {
AddMemoryOptions,
AssistantMessage,
FileListResponse,
FileMetadata,
FileUploadResponse,
GetApiServerStatusResult,
KnowledgeBaseParams,
KnowledgeItem,
KnowledgeSearchResult,
MCPServer,
MemoryConfig,
MemoryListOptions,
MemorySearchOptions,
OcrProvider,
OcrResult,
Provider,
RestartApiServerStatusResult,
S3Config,
Shortcut,
StartApiServerStatusResult,
StopApiServerStatusResult,
SupportedOcrFile,
ThemeMode,
WebDavConfig
} from '@types'
import type { OpenDialogOptions } from 'electron'
import { contextBridge, ipcRenderer, shell, webUtils } from 'electron'
import type { CreateDirectoryOptions } from 'webdav'
import type {
InstalledPlugin,
InstallPluginOptions,
ListAvailablePluginsResult,
PluginMetadata,
PluginResult,
UninstallPluginOptions,
WritePluginContentOptions
} from '../renderer/src/types/plugin'
import type { ActionItem } from '../renderer/src/types/selectionTypes'
type DirectoryListOptions = {
recursive?: boolean
maxDepth?: number
includeHidden?: boolean
includeFiles?: boolean
includeDirectories?: boolean
maxEntries?: number
searchPattern?: string
}
export function tracedInvoke(channel: string, spanContext: SpanContext | undefined, ...args: any[]) {
if (spanContext) {
const data = { type: 'trace', context: spanContext }
return ipcRenderer.invoke(channel, ...args, data)
}
return ipcRenderer.invoke(channel, ...args)
}
// Custom APIs for renderer
const api = {
getAppInfo: () => ipcRenderer.invoke(IpcChannel.App_Info),
getDiskInfo: (directoryPath: string): Promise<{ free: number; size: number } | null> =>
ipcRenderer.invoke(IpcChannel.App_GetDiskInfo, directoryPath),
reload: () => ipcRenderer.invoke(IpcChannel.App_Reload),
quit: () => ipcRenderer.invoke(IpcChannel.App_Quit),
setProxy: (proxy: string | undefined, bypassRules?: string) =>
ipcRenderer.invoke(IpcChannel.App_Proxy, proxy, bypassRules),
checkForUpdate: () => ipcRenderer.invoke(IpcChannel.App_CheckForUpdate),
quitAndInstall: () => ipcRenderer.invoke(IpcChannel.App_QuitAndInstall),
setLanguage: (lang: string) => ipcRenderer.invoke(IpcChannel.App_SetLanguage, lang),
setEnableSpellCheck: (isEnable: boolean) => ipcRenderer.invoke(IpcChannel.App_SetEnableSpellCheck, isEnable),
setSpellCheckLanguages: (languages: string[]) => ipcRenderer.invoke(IpcChannel.App_SetSpellCheckLanguages, languages),
setLaunchOnBoot: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetLaunchOnBoot, isActive),
setLaunchToTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetLaunchToTray, isActive),
setTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTray, isActive),
setTrayOnClose: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTrayOnClose, isActive),
setTestPlan: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTestPlan, isActive),
setTestChannel: (channel: UpgradeChannel) => ipcRenderer.invoke(IpcChannel.App_SetTestChannel, channel),
setTheme: (theme: ThemeMode) => ipcRenderer.invoke(IpcChannel.App_SetTheme, theme),
handleZoomFactor: (delta: number, reset: boolean = false) =>
ipcRenderer.invoke(IpcChannel.App_HandleZoomFactor, delta, reset),
setAutoUpdate: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetAutoUpdate, isActive),
select: (options: Electron.OpenDialogOptions) => ipcRenderer.invoke(IpcChannel.App_Select, options),
hasWritePermission: (path: string) => ipcRenderer.invoke(IpcChannel.App_HasWritePermission, path),
resolvePath: (path: string) => ipcRenderer.invoke(IpcChannel.App_ResolvePath, path),
isPathInside: (childPath: string, parentPath: string) =>
ipcRenderer.invoke(IpcChannel.App_IsPathInside, childPath, parentPath),
setAppDataPath: (path: string) => ipcRenderer.invoke(IpcChannel.App_SetAppDataPath, path),
getDataPathFromArgs: () => ipcRenderer.invoke(IpcChannel.App_GetDataPathFromArgs),
copy: (oldPath: string, newPath: string, occupiedDirs: string[] = []) =>
ipcRenderer.invoke(IpcChannel.App_Copy, oldPath, newPath, occupiedDirs),
setStopQuitApp: (stop: boolean, reason: string) => ipcRenderer.invoke(IpcChannel.App_SetStopQuitApp, stop, reason),
flushAppData: () => ipcRenderer.invoke(IpcChannel.App_FlushAppData),
isNotEmptyDir: (path: string) => ipcRenderer.invoke(IpcChannel.App_IsNotEmptyDir, path),
relaunchApp: (options?: Electron.RelaunchOptions) => ipcRenderer.invoke(IpcChannel.App_RelaunchApp, options),
openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url),
getCacheSize: () => ipcRenderer.invoke(IpcChannel.App_GetCacheSize),
clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache),
logToMain: (source: LogSourceWithContext, level: LogLevel, message: string, data: any[]) =>
ipcRenderer.invoke(IpcChannel.App_LogToMain, source, level, message, data),
setFullScreen: (value: boolean): Promise<void> => ipcRenderer.invoke(IpcChannel.App_SetFullScreen, value),
isFullScreen: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.App_IsFullScreen),
getSystemFonts: (): Promise<string[]> => ipcRenderer.invoke(IpcChannel.App_GetSystemFonts),
mac: {
isProcessTrusted: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.App_MacIsProcessTrusted),
requestProcessTrust: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.App_MacRequestProcessTrust)
},
notification: {
send: (notification: Notification) => ipcRenderer.invoke(IpcChannel.Notification_Send, notification)
},
system: {
getDeviceType: () => ipcRenderer.invoke(IpcChannel.System_GetDeviceType),
getHostname: () => ipcRenderer.invoke(IpcChannel.System_GetHostname),
getCpuName: () => ipcRenderer.invoke(IpcChannel.System_GetCpuName)
},
devTools: {
toggle: () => ipcRenderer.invoke(IpcChannel.System_ToggleDevTools)
},
zip: {
compress: (text: string) => ipcRenderer.invoke(IpcChannel.Zip_Compress, text),
decompress: (text: Buffer) => ipcRenderer.invoke(IpcChannel.Zip_Decompress, text)
},
backup: {
backup: (filename: string, content: string, path: string, skipBackupFile: boolean) =>
ipcRenderer.invoke(IpcChannel.Backup_Backup, filename, content, path, skipBackupFile),
restore: (path: string) => ipcRenderer.invoke(IpcChannel.Backup_Restore, path),
backupToWebdav: (data: string, webdavConfig: WebDavConfig) =>
ipcRenderer.invoke(IpcChannel.Backup_BackupToWebdav, data, webdavConfig),
restoreFromWebdav: (webdavConfig: WebDavConfig) =>
ipcRenderer.invoke(IpcChannel.Backup_RestoreFromWebdav, webdavConfig),
listWebdavFiles: (webdavConfig: WebDavConfig) =>
ipcRenderer.invoke(IpcChannel.Backup_ListWebdavFiles, webdavConfig),
checkConnection: (webdavConfig: WebDavConfig) =>
ipcRenderer.invoke(IpcChannel.Backup_CheckConnection, webdavConfig),
createDirectory: (webdavConfig: WebDavConfig, path: string, options?: CreateDirectoryOptions) =>
ipcRenderer.invoke(IpcChannel.Backup_CreateDirectory, webdavConfig, path, options),
deleteWebdavFile: (fileName: string, webdavConfig: WebDavConfig) =>
ipcRenderer.invoke(IpcChannel.Backup_DeleteWebdavFile, fileName, webdavConfig),
backupToLocalDir: (
data: string,
fileName: string,
localConfig: { localBackupDir?: string; skipBackupFile?: boolean }
) => ipcRenderer.invoke(IpcChannel.Backup_BackupToLocalDir, data, fileName, localConfig),
restoreFromLocalBackup: (fileName: string, localBackupDir?: string) =>
ipcRenderer.invoke(IpcChannel.Backup_RestoreFromLocalBackup, fileName, localBackupDir),
listLocalBackupFiles: (localBackupDir?: string) =>
ipcRenderer.invoke(IpcChannel.Backup_ListLocalBackupFiles, localBackupDir),
deleteLocalBackupFile: (fileName: string, localBackupDir?: string) =>
ipcRenderer.invoke(IpcChannel.Backup_DeleteLocalBackupFile, fileName, localBackupDir),
checkWebdavConnection: (webdavConfig: WebDavConfig) =>
ipcRenderer.invoke(IpcChannel.Backup_CheckConnection, webdavConfig),
backupToS3: (data: string, s3Config: S3Config) => ipcRenderer.invoke(IpcChannel.Backup_BackupToS3, data, s3Config),
restoreFromS3: (s3Config: S3Config) => ipcRenderer.invoke(IpcChannel.Backup_RestoreFromS3, s3Config),
listS3Files: (s3Config: S3Config) => ipcRenderer.invoke(IpcChannel.Backup_ListS3Files, s3Config),
deleteS3File: (fileName: string, s3Config: S3Config) =>
ipcRenderer.invoke(IpcChannel.Backup_DeleteS3File, fileName, s3Config),
checkS3Connection: (s3Config: S3Config) => ipcRenderer.invoke(IpcChannel.Backup_CheckS3Connection, s3Config)
},
file: {
select: (options?: OpenDialogOptions): Promise<FileMetadata[] | null> =>
ipcRenderer.invoke(IpcChannel.File_Select, options),
upload: (file: FileMetadata) => ipcRenderer.invoke(IpcChannel.File_Upload, file),
delete: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Delete, fileId),
deleteDir: (dirPath: string) => ipcRenderer.invoke(IpcChannel.File_DeleteDir, dirPath),
deleteExternalFile: (filePath: string) => ipcRenderer.invoke(IpcChannel.File_DeleteExternalFile, filePath),
deleteExternalDir: (dirPath: string) => ipcRenderer.invoke(IpcChannel.File_DeleteExternalDir, dirPath),
move: (path: string, newPath: string) => ipcRenderer.invoke(IpcChannel.File_Move, path, newPath),
moveDir: (dirPath: string, newDirPath: string) => ipcRenderer.invoke(IpcChannel.File_MoveDir, dirPath, newDirPath),
rename: (path: string, newName: string) => ipcRenderer.invoke(IpcChannel.File_Rename, path, newName),
renameDir: (dirPath: string, newName: string) => ipcRenderer.invoke(IpcChannel.File_RenameDir, dirPath, newName),
read: (fileId: string, detectEncoding?: boolean) =>
ipcRenderer.invoke(IpcChannel.File_Read, fileId, detectEncoding),
readExternal: (filePath: string, detectEncoding?: boolean) =>
ipcRenderer.invoke(IpcChannel.File_ReadExternal, filePath, detectEncoding),
clear: (spanContext?: SpanContext) => ipcRenderer.invoke(IpcChannel.File_Clear, spanContext),
get: (filePath: string): Promise<FileMetadata | null> => ipcRenderer.invoke(IpcChannel.File_Get, filePath),
createTempFile: (fileName: string): Promise<string> => ipcRenderer.invoke(IpcChannel.File_CreateTempFile, fileName),
mkdir: (dirPath: string) => ipcRenderer.invoke(IpcChannel.File_Mkdir, dirPath),
write: (filePath: string, data: Uint8Array | string) => ipcRenderer.invoke(IpcChannel.File_Write, filePath, data),
writeWithId: (id: string, content: string) => ipcRenderer.invoke(IpcChannel.File_WriteWithId, id, content),
open: (options?: OpenDialogOptions) => ipcRenderer.invoke(IpcChannel.File_Open, options),
openPath: (path: string) => ipcRenderer.invoke(IpcChannel.File_OpenPath, path),
save: (path: string, content: string | NodeJS.ArrayBufferView, options?: any) =>
ipcRenderer.invoke(IpcChannel.File_Save, path, content, options),
selectFolder: (options?: OpenDialogOptions): Promise<string | null> =>
ipcRenderer.invoke(IpcChannel.File_SelectFolder, options),
saveImage: (name: string, data: string) => ipcRenderer.invoke(IpcChannel.File_SaveImage, name, data),
binaryImage: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_BinaryImage, fileId),
base64Image: (fileId: string): Promise<{ mime: string; base64: string; data: string }> =>
ipcRenderer.invoke(IpcChannel.File_Base64Image, fileId),
saveBase64Image: (data: string) => ipcRenderer.invoke(IpcChannel.File_SaveBase64Image, data),
savePastedImage: (imageData: Uint8Array, extension?: string) =>
ipcRenderer.invoke(IpcChannel.File_SavePastedImage, imageData, extension),
download: (url: string, isUseContentType?: boolean) =>
ipcRenderer.invoke(IpcChannel.File_Download, url, isUseContentType),
copy: (fileId: string, destPath: string) => ipcRenderer.invoke(IpcChannel.File_Copy, fileId, destPath),
base64File: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64File, fileId),
pdfInfo: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_GetPdfInfo, fileId),
getPathForFile: (file: File) => webUtils.getPathForFile(file),
openFileWithRelativePath: (file: FileMetadata) => ipcRenderer.invoke(IpcChannel.File_OpenWithRelativePath, file),
isTextFile: (filePath: string): Promise<boolean> => ipcRenderer.invoke(IpcChannel.File_IsTextFile, filePath),
getDirectoryStructure: (dirPath: string) => ipcRenderer.invoke(IpcChannel.File_GetDirectoryStructure, dirPath),
listDirectory: (dirPath: string, options?: DirectoryListOptions) =>
ipcRenderer.invoke(IpcChannel.File_ListDirectory, dirPath, options),
checkFileName: (dirPath: string, fileName: string, isFile: boolean) =>
ipcRenderer.invoke(IpcChannel.File_CheckFileName, dirPath, fileName, isFile),
validateNotesDirectory: (dirPath: string) => ipcRenderer.invoke(IpcChannel.File_ValidateNotesDirectory, dirPath),
startFileWatcher: (dirPath: string, config?: any) =>
ipcRenderer.invoke(IpcChannel.File_StartWatcher, dirPath, config),
stopFileWatcher: () => ipcRenderer.invoke(IpcChannel.File_StopWatcher),
onFileChange: (callback: (data: FileChangeEvent) => void) => {
const listener = (_event: Electron.IpcRendererEvent, data: any) => {
if (data && typeof data === 'object') {
callback(data)
}
}
ipcRenderer.on('file-change', listener)
return () => ipcRenderer.off('file-change', listener)
},
showInFolder: (path: string): Promise<void> => ipcRenderer.invoke(IpcChannel.File_ShowInFolder, path)
},
fs: {
read: (pathOrUrl: string, encoding?: BufferEncoding) => ipcRenderer.invoke(IpcChannel.Fs_Read, pathOrUrl, encoding),
readText: (pathOrUrl: string): Promise<string> => ipcRenderer.invoke(IpcChannel.Fs_ReadText, pathOrUrl)
},
export: {
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke(IpcChannel.Export_Word, markdown, fileName)
},
obsidian: {
getVaults: () => ipcRenderer.invoke(IpcChannel.Obsidian_GetVaults),
getFolders: (vaultName: string) => ipcRenderer.invoke(IpcChannel.Obsidian_GetFiles, vaultName),
getFiles: (vaultName: string) => ipcRenderer.invoke(IpcChannel.Obsidian_GetFiles, vaultName)
},
openPath: (path: string) => ipcRenderer.invoke(IpcChannel.Open_Path, path),
shortcuts: {
update: (shortcuts: Shortcut[]) => ipcRenderer.invoke(IpcChannel.Shortcuts_Update, shortcuts)
},
knowledgeBase: {
create: (base: KnowledgeBaseParams, context?: SpanContext) =>
tracedInvoke(IpcChannel.KnowledgeBase_Create, context, base),
reset: (base: KnowledgeBaseParams) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Reset, base),
delete: (id: string) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Delete, id),
add: ({
base,
item,
userId,
forceReload = false
}: {
base: KnowledgeBaseParams
item: KnowledgeItem
userId?: string
forceReload?: boolean
}) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Add, { base, item, forceReload, userId }),
remove: ({ uniqueId, uniqueIds, base }: { uniqueId: string; uniqueIds: string[]; base: KnowledgeBaseParams }) =>
ipcRenderer.invoke(IpcChannel.KnowledgeBase_Remove, { uniqueId, uniqueIds, base }),
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }, context?: SpanContext) =>
tracedInvoke(IpcChannel.KnowledgeBase_Search, context, { search, base }),
rerank: (
{ search, base, results }: { search: string; base: KnowledgeBaseParams; results: KnowledgeSearchResult[] },
context?: SpanContext
) => tracedInvoke(IpcChannel.KnowledgeBase_Rerank, context, { search, base, results }),
checkQuota: ({ base, userId }: { base: KnowledgeBaseParams; userId: string }) =>
ipcRenderer.invoke(IpcChannel.KnowledgeBase_Check_Quota, base, userId)
},
memory: {
add: (messages: string | AssistantMessage[], options?: AddMemoryOptions) =>
ipcRenderer.invoke(IpcChannel.Memory_Add, messages, options),
search: (query: string, options: MemorySearchOptions) =>
ipcRenderer.invoke(IpcChannel.Memory_Search, query, options),
list: (options?: MemoryListOptions) => ipcRenderer.invoke(IpcChannel.Memory_List, options),
delete: (id: string) => ipcRenderer.invoke(IpcChannel.Memory_Delete, id),
update: (id: string, memory: string, metadata?: Record<string, any>) =>
ipcRenderer.invoke(IpcChannel.Memory_Update, id, memory, metadata),
get: (id: string) => ipcRenderer.invoke(IpcChannel.Memory_Get, id),
setConfig: (config: MemoryConfig) => ipcRenderer.invoke(IpcChannel.Memory_SetConfig, config),
deleteUser: (userId: string) => ipcRenderer.invoke(IpcChannel.Memory_DeleteUser, userId),
deleteAllMemoriesForUser: (userId: string) =>
ipcRenderer.invoke(IpcChannel.Memory_DeleteAllMemoriesForUser, userId),
getUsersList: () => ipcRenderer.invoke(IpcChannel.Memory_GetUsersList)
},
window: {
setMinimumSize: (width: number, height: number) =>
ipcRenderer.invoke(IpcChannel.Windows_SetMinimumSize, width, height),
resetMinimumSize: () => ipcRenderer.invoke(IpcChannel.Windows_ResetMinimumSize),
getSize: (): Promise<[number, number]> => ipcRenderer.invoke(IpcChannel.Windows_GetSize)
},
fileService: {
upload: (provider: Provider, file: FileMetadata): Promise<FileUploadResponse> =>
ipcRenderer.invoke(IpcChannel.FileService_Upload, provider, file),
list: (provider: Provider): Promise<FileListResponse> => ipcRenderer.invoke(IpcChannel.FileService_List, provider),
delete: (provider: Provider, fileId: string) => ipcRenderer.invoke(IpcChannel.FileService_Delete, provider, fileId),
retrieve: (provider: Provider, fileId: string): Promise<FileUploadResponse> =>
ipcRenderer.invoke(IpcChannel.FileService_Retrieve, provider, fileId)
},
selectionMenu: {
action: (action: string) => ipcRenderer.invoke('selection-menu:action', action)
},
vertexAI: {
getAuthHeaders: (params: { projectId: string; serviceAccount?: { privateKey: string; clientEmail: string } }) =>
ipcRenderer.invoke(IpcChannel.VertexAI_GetAuthHeaders, params),
getAccessToken: (params: { projectId: string; serviceAccount?: { privateKey: string; clientEmail: string } }) =>
ipcRenderer.invoke(IpcChannel.VertexAI_GetAccessToken, params),
clearAuthCache: (projectId: string, clientEmail?: string) =>
ipcRenderer.invoke(IpcChannel.VertexAI_ClearAuthCache, projectId, clientEmail)
},
ovms: {
addModel: (modelName: string, modelId: string, modelSource: string, task: string) =>
ipcRenderer.invoke(IpcChannel.Ovms_AddModel, modelName, modelId, modelSource, task),
stopAddModel: () => ipcRenderer.invoke(IpcChannel.Ovms_StopAddModel),
getModels: () => ipcRenderer.invoke(IpcChannel.Ovms_GetModels),
isRunning: () => ipcRenderer.invoke(IpcChannel.Ovms_IsRunning),
getStatus: () => ipcRenderer.invoke(IpcChannel.Ovms_GetStatus),
runOvms: () => ipcRenderer.invoke(IpcChannel.Ovms_RunOVMS),
stopOvms: () => ipcRenderer.invoke(IpcChannel.Ovms_StopOVMS)
},
config: {
set: (key: string, value: any, isNotify: boolean = false) =>
ipcRenderer.invoke(IpcChannel.Config_Set, key, value, isNotify),
get: (key: string) => ipcRenderer.invoke(IpcChannel.Config_Get, key)
},
miniWindow: {
show: () => ipcRenderer.invoke(IpcChannel.MiniWindow_Show),
hide: () => ipcRenderer.invoke(IpcChannel.MiniWindow_Hide),
close: () => ipcRenderer.invoke(IpcChannel.MiniWindow_Close),
toggle: () => ipcRenderer.invoke(IpcChannel.MiniWindow_Toggle),
setPin: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.MiniWindow_SetPin, isPinned)
},
aes: {
encrypt: (text: string, secretKey: string, iv: string) =>
ipcRenderer.invoke(IpcChannel.Aes_Encrypt, text, secretKey, iv),
decrypt: (encryptedData: string, iv: string, secretKey: string) =>
ipcRenderer.invoke(IpcChannel.Aes_Decrypt, encryptedData, iv, secretKey)
},
mcp: {
removeServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_RemoveServer, server),
restartServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_RestartServer, server),
stopServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_StopServer, server),
listTools: (server: MCPServer, context?: SpanContext) => tracedInvoke(IpcChannel.Mcp_ListTools, context, server),
callTool: (
{ server, name, args, callId }: { server: MCPServer; name: string; args: any; callId?: string },
context?: SpanContext
) => tracedInvoke(IpcChannel.Mcp_CallTool, context, { server, name, args, callId }),
listPrompts: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListPrompts, server),
getPrompt: ({ server, name, args }: { server: MCPServer; name: string; args?: Record<string, any> }) =>
ipcRenderer.invoke(IpcChannel.Mcp_GetPrompt, { server, name, args }),
listResources: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListResources, server),
getResource: ({ server, uri }: { server: MCPServer; uri: string }) =>
ipcRenderer.invoke(IpcChannel.Mcp_GetResource, { server, uri }),
getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo),
checkMcpConnectivity: (server: any) => ipcRenderer.invoke(IpcChannel.Mcp_CheckConnectivity, server),
uploadDxt: async (file: File) => {
const buffer = await file.arrayBuffer()
return ipcRenderer.invoke(IpcChannel.Mcp_UploadDxt, buffer, file.name)
},
abortTool: (callId: string) => ipcRenderer.invoke(IpcChannel.Mcp_AbortTool, callId),
getServerVersion: (server: MCPServer): Promise<string | null> =>
ipcRenderer.invoke(IpcChannel.Mcp_GetServerVersion, server)
},
python: {
execute: (script: string, context?: Record<string, any>, timeout?: number) =>
ipcRenderer.invoke(IpcChannel.Python_Execute, script, context, timeout)
},
shell: {
openExternal: (url: string, options?: Electron.OpenExternalOptions) => shell.openExternal(url, options)
},
copilot: {
getAuthMessage: (headers?: Record<string, string>) =>
ipcRenderer.invoke(IpcChannel.Copilot_GetAuthMessage, headers),
getCopilotToken: (device_code: string, headers?: Record<string, string>) =>
ipcRenderer.invoke(IpcChannel.Copilot_GetCopilotToken, device_code, headers),
saveCopilotToken: (access_token: string) => ipcRenderer.invoke(IpcChannel.Copilot_SaveCopilotToken, access_token),
getToken: (headers?: Record<string, string>) => ipcRenderer.invoke(IpcChannel.Copilot_GetToken, headers),
logout: () => ipcRenderer.invoke(IpcChannel.Copilot_Logout),
getUser: (token: string) => ipcRenderer.invoke(IpcChannel.Copilot_GetUser, token)
},
// Binary related APIs
isBinaryExist: (name: string) => ipcRenderer.invoke(IpcChannel.App_IsBinaryExist, name),
getBinaryPath: (name: string) => ipcRenderer.invoke(IpcChannel.App_GetBinaryPath, name),
installUVBinary: () => ipcRenderer.invoke(IpcChannel.App_InstallUvBinary),
installBunBinary: () => ipcRenderer.invoke(IpcChannel.App_InstallBunBinary),
installOvmsBinary: () => ipcRenderer.invoke(IpcChannel.App_InstallOvmsBinary),
protocol: {
onReceiveData: (callback: (data: { url: string; params: any }) => void) => {
const listener = (_event: Electron.IpcRendererEvent, data: { url: string; params: any }) => {
callback(data)
}
ipcRenderer.on('protocol-data', listener)
return () => {
ipcRenderer.off('protocol-data', listener)
}
}
},
nutstore: {
getSSOUrl: () => ipcRenderer.invoke(IpcChannel.Nutstore_GetSsoUrl),
decryptToken: (token: string) => ipcRenderer.invoke(IpcChannel.Nutstore_DecryptToken, token),
getDirectoryContents: (token: string, path: string) =>
ipcRenderer.invoke(IpcChannel.Nutstore_GetDirectoryContents, token, path)
},
searchService: {
openSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Open, uid),
closeSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Close, uid),
openUrlInSearchWindow: (uid: string, url: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_OpenUrl, uid, url)
},
webview: {
setOpenLinkExternal: (webviewId: number, isExternal: boolean) =>
ipcRenderer.invoke(IpcChannel.Webview_SetOpenLinkExternal, webviewId, isExternal),
setSpellCheckEnabled: (webviewId: number, isEnable: boolean) =>
ipcRenderer.invoke(IpcChannel.Webview_SetSpellCheckEnabled, webviewId, isEnable),
onFindShortcut: (callback: (payload: WebviewKeyEvent) => void) => {
const listener = (_event: Electron.IpcRendererEvent, payload: WebviewKeyEvent) => {
callback(payload)
}
ipcRenderer.on(IpcChannel.Webview_SearchHotkey, listener)
return () => {
ipcRenderer.off(IpcChannel.Webview_SearchHotkey, listener)
}
}
},
storeSync: {
subscribe: () => ipcRenderer.invoke(IpcChannel.StoreSync_Subscribe),
unsubscribe: () => ipcRenderer.invoke(IpcChannel.StoreSync_Unsubscribe),
onUpdate: (action: any) => ipcRenderer.invoke(IpcChannel.StoreSync_OnUpdate, action)
},
selection: {
hideToolbar: () => ipcRenderer.invoke(IpcChannel.Selection_ToolbarHide),
writeToClipboard: (text: string) => ipcRenderer.invoke(IpcChannel.Selection_WriteToClipboard, text),
determineToolbarSize: (width: number, height: number) =>
ipcRenderer.invoke(IpcChannel.Selection_ToolbarDetermineSize, width, height),
setEnabled: (enabled: boolean) => ipcRenderer.invoke(IpcChannel.Selection_SetEnabled, enabled),
setTriggerMode: (triggerMode: string) => ipcRenderer.invoke(IpcChannel.Selection_SetTriggerMode, triggerMode),
setFollowToolbar: (isFollowToolbar: boolean) =>
ipcRenderer.invoke(IpcChannel.Selection_SetFollowToolbar, isFollowToolbar),
setRemeberWinSize: (isRemeberWinSize: boolean) =>
ipcRenderer.invoke(IpcChannel.Selection_SetRemeberWinSize, isRemeberWinSize),
setFilterMode: (filterMode: string) => ipcRenderer.invoke(IpcChannel.Selection_SetFilterMode, filterMode),
setFilterList: (filterList: string[]) => ipcRenderer.invoke(IpcChannel.Selection_SetFilterList, filterList),
processAction: (actionItem: ActionItem, isFullScreen: boolean = false) =>
ipcRenderer.invoke(IpcChannel.Selection_ProcessAction, actionItem, isFullScreen),
closeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowClose),
minimizeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowMinimize),
pinActionWindow: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowPin, isPinned)
},
agentTools: {
respondToPermission: (payload: {
requestId: string
behavior: 'allow' | 'deny'
updatedInput?: Record<string, unknown>
message?: string
updatedPermissions?: PermissionUpdate[]
}) => ipcRenderer.invoke(IpcChannel.AgentToolPermission_Response, payload)
},
quoteToMainWindow: (text: string) => ipcRenderer.invoke(IpcChannel.App_QuoteToMain, text),
setDisableHardwareAcceleration: (isDisable: boolean) =>
ipcRenderer.invoke(IpcChannel.App_SetDisableHardwareAcceleration, isDisable),
trace: {
saveData: (topicId: string) => ipcRenderer.invoke(IpcChannel.TRACE_SAVE_DATA, topicId),
getData: (topicId: string, traceId: string, modelName?: string) =>
ipcRenderer.invoke(IpcChannel.TRACE_GET_DATA, topicId, traceId, modelName),
saveEntity: (entity: SpanEntity) => ipcRenderer.invoke(IpcChannel.TRACE_SAVE_ENTITY, entity),
getEntity: (spanId: string) => ipcRenderer.invoke(IpcChannel.TRACE_GET_ENTITY, spanId),
bindTopic: (topicId: string, traceId: string) => ipcRenderer.invoke(IpcChannel.TRACE_BIND_TOPIC, topicId, traceId),
tokenUsage: (spanId: string, usage: TokenUsage) => ipcRenderer.invoke(IpcChannel.TRACE_TOKEN_USAGE, spanId, usage),
cleanHistory: (topicId: string, traceId: string, modelName?: string) =>
ipcRenderer.invoke(IpcChannel.TRACE_CLEAN_HISTORY, topicId, traceId, modelName),
cleanTopic: (topicId: string, traceId?: string) =>
ipcRenderer.invoke(IpcChannel.TRACE_CLEAN_TOPIC, topicId, traceId),
openWindow: (topicId: string, traceId: string, autoOpen?: boolean, modelName?: string) =>
ipcRenderer.invoke(IpcChannel.TRACE_OPEN_WINDOW, topicId, traceId, autoOpen, modelName),
setTraceWindowTitle: (title: string) => ipcRenderer.invoke(IpcChannel.TRACE_SET_TITLE, title),
addEndMessage: (spanId: string, modelName: string, context: string) =>
ipcRenderer.invoke(IpcChannel.TRACE_ADD_END_MESSAGE, spanId, modelName, context),
cleanLocalData: () => ipcRenderer.invoke(IpcChannel.TRACE_CLEAN_LOCAL_DATA),
addStreamMessage: (spanId: string, modelName: string, context: string, message: any) =>
ipcRenderer.invoke(IpcChannel.TRACE_ADD_STREAM_MESSAGE, spanId, modelName, context, message)
},
anthropic_oauth: {
startOAuthFlow: () => ipcRenderer.invoke(IpcChannel.Anthropic_StartOAuthFlow),
completeOAuthWithCode: (code: string) => ipcRenderer.invoke(IpcChannel.Anthropic_CompleteOAuthWithCode, code),
cancelOAuthFlow: () => ipcRenderer.invoke(IpcChannel.Anthropic_CancelOAuthFlow),
getAccessToken: () => ipcRenderer.invoke(IpcChannel.Anthropic_GetAccessToken),
hasCredentials: () => ipcRenderer.invoke(IpcChannel.Anthropic_HasCredentials),
clearCredentials: () => ipcRenderer.invoke(IpcChannel.Anthropic_ClearCredentials)
},
codeTools: {
run: (
cliTool: string,
model: string,
directory: string,
env: Record<string, string>,
options?: { autoUpdateToLatest?: boolean; terminal?: string }
) => ipcRenderer.invoke(IpcChannel.CodeTools_Run, cliTool, model, directory, env, options),
getAvailableTerminals: (): Promise<TerminalConfig[]> =>
ipcRenderer.invoke(IpcChannel.CodeTools_GetAvailableTerminals),
setCustomTerminalPath: (terminalId: string, path: string): Promise<void> =>
ipcRenderer.invoke(IpcChannel.CodeTools_SetCustomTerminalPath, terminalId, path),
getCustomTerminalPath: (terminalId: string): Promise<string | undefined> =>
ipcRenderer.invoke(IpcChannel.CodeTools_GetCustomTerminalPath, terminalId),
removeCustomTerminalPath: (terminalId: string): Promise<void> =>
ipcRenderer.invoke(IpcChannel.CodeTools_RemoveCustomTerminalPath, terminalId)
},
ocr: {
ocr: (file: SupportedOcrFile, provider: OcrProvider): Promise<OcrResult> =>
ipcRenderer.invoke(IpcChannel.OCR_ocr, file, provider),
listProviders: (): Promise<string[]> => ipcRenderer.invoke(IpcChannel.OCR_ListProviders)
},
cherryai: {
generateSignature: (params: { method: string; path: string; query: string; body: Record<string, any> }) =>
ipcRenderer.invoke(IpcChannel.Cherryai_GetSignature, params)
},
windowControls: {
minimize: (): Promise<void> => ipcRenderer.invoke(IpcChannel.Windows_Minimize),
maximize: (): Promise<void> => ipcRenderer.invoke(IpcChannel.Windows_Maximize),
unmaximize: (): Promise<void> => ipcRenderer.invoke(IpcChannel.Windows_Unmaximize),
close: (): Promise<void> => ipcRenderer.invoke(IpcChannel.Windows_Close),
isMaximized: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.Windows_IsMaximized),
onMaximizedChange: (callback: (isMaximized: boolean) => void): (() => void) => {
const channel = IpcChannel.Windows_MaximizedChanged
const listener = (_: Electron.IpcRendererEvent, isMaximized: boolean) => callback(isMaximized)
ipcRenderer.on(channel, listener)
return () => {
ipcRenderer.removeListener(channel, listener)
}
}
},
apiServer: {
getStatus: (): Promise<GetApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_GetStatus),
start: (): Promise<StartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Start),
restart: (): Promise<RestartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Restart),
stop: (): Promise<StopApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Stop),
onReady: (callback: () => void): (() => void) => {
const listener = () => {
callback()
}
ipcRenderer.on(IpcChannel.ApiServer_Ready, listener)
return () => {
ipcRenderer.removeListener(IpcChannel.ApiServer_Ready, listener)
}
}
},
claudeCodePlugin: {
listAvailable: (): Promise<PluginResult<ListAvailablePluginsResult>> =>
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_ListAvailable),
install: (options: InstallPluginOptions): Promise<PluginResult<PluginMetadata>> =>
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_Install, options),
uninstall: (options: UninstallPluginOptions): Promise<PluginResult<void>> =>
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_Uninstall, options),
listInstalled: (agentId: string): Promise<PluginResult<InstalledPlugin[]>> =>
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_ListInstalled, agentId),
invalidateCache: (): Promise<PluginResult<void>> => ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_InvalidateCache),
readContent: (sourcePath: string): Promise<PluginResult<string>> =>
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_ReadContent, sourcePath),
writeContent: (options: WritePluginContentOptions): Promise<PluginResult<void>> =>
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_WriteContent, options)
},
webSocket: {
start: () => ipcRenderer.invoke(IpcChannel.WebSocket_Start),
stop: () => ipcRenderer.invoke(IpcChannel.WebSocket_Stop),
status: () => ipcRenderer.invoke(IpcChannel.WebSocket_Status),
sendFile: (filePath: string) => ipcRenderer.invoke(IpcChannel.WebSocket_SendFile, filePath),
getAllCandidates: () => ipcRenderer.invoke(IpcChannel.WebSocket_GetAllCandidates)
}
}
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
} catch (error) {
console.error('[Preload]Failed to expose APIs:', error as Error)
}
} else {
window.electron = electronAPI
window.api = api
}
export type WindowApiType = typeof api