index.ts
1 import { app, shell, WebContents, RenderProcessGoneDetails, BrowserWindow } from 'electron' 2 import { v4 as uuidv4 } from 'uuid' 3 import Constants from './utils/Constants' 4 import { createErrorWindow, createMainWindow, createSplashWindow } from './MainRunner' 5 6 import { 7 SamplingRequest, 8 SamplingResponse, 9 ElicitRequest, 10 ElicitResponse, 11 CommandRequest 12 } from './types' 13 14 import { McpProgressCallbackObject } from './mcp/types' 15 16 import { 17 IpcSamplingRequest, 18 IpcElicitRequest, 19 IpcCommandRequest, 20 IpcMcpInitRequest 21 } from '@/types/ipc' 22 23 import { listenOnceForRendererResponse } from './IPCs' 24 25 import * as shortcuts from './aid/shortcuts' 26 import Commander from './aid/commander' 27 28 import { showWindow } from './tray' 29 30 let splashWindow: BrowserWindow 31 let mainWindow: BrowserWindow 32 let errorWindow: BrowserWindow 33 34 app.setAppUserModelId(Constants.APP_NAME) 35 36 const registerShortcuts = async () => { 37 const initState = await Commander.init() 38 if (initState) { 39 shortcuts.registerShortcuts({ 40 command: () => Commander.initCommand() 41 }) 42 } 43 } 44 45 async function createWindow() { 46 try { 47 splashWindow = await createSplashWindow() 48 mainWindow = await createMainWindow() 49 } catch { 50 app.exit() 51 } finally { 52 if (splashWindow) { 53 splashWindow.close() 54 splashWindow = null 55 } 56 } 57 } 58 59 app.on('ready', async () => { 60 // Disable special menus on macOS by uncommenting the following, if necessary 61 /* 62 if (Constants.IS_MAC) { 63 systemPreferences.setUserDefault('NSDisabledDictationMenuItem', 'boolean', true) 64 systemPreferences.setUserDefault('NSDisabledCharacterPaletteMenuItem', 'boolean', true) 65 } 66 */ 67 68 createWindow() 69 registerShortcuts() 70 }) 71 72 app.on('activate', async () => { 73 if (!mainWindow) { 74 createWindow() 75 } 76 }) 77 78 app.on('window-all-closed', () => { 79 mainWindow = null 80 splashWindow = null 81 errorWindow = null 82 83 if (!Constants.IS_MAC) { 84 app.quit() 85 } 86 }) 87 88 app.on('web-contents-created', (e, webContents) => { 89 webContents.setWindowOpenHandler(({ url }) => { 90 shell.openExternal(url) 91 return { action: 'deny' } 92 }) 93 94 // This will not affect hash/history navigation since only 95 // did-start-navigation and did-navigate-in-page will be 96 // triggered 97 webContents.on('will-navigate', (event, url) => { 98 const currentUrl = webContents.getURL() 99 let currentHost, targetHost 100 101 try { 102 currentHost = new URL(currentUrl).host 103 targetHost = new URL(url).host 104 } catch (_error) { 105 // Invalid URL should be opened externally 106 event.preventDefault() 107 shell.openExternal(url) 108 return 109 } 110 111 // Allow reload on same Host, such as vite index reload 112 if (Constants.IS_DEV_ENV && currentHost === targetHost) { 113 return 114 } 115 116 // Other URL should be opened externally 117 event.preventDefault() 118 shell.openExternal(url) 119 }) 120 }) 121 122 app.on( 123 'render-process-gone', 124 async (event: Event, webContents: WebContents, details: RenderProcessGoneDetails) => { 125 errorWindow = await createErrorWindow(errorWindow, mainWindow, details) 126 } 127 ) 128 129 process.on('uncaughtException', async () => { 130 errorWindow = await createErrorWindow(errorWindow, mainWindow) 131 }) 132 133 const msgSamplingTransferResultChannel = 'msgSamplingTransferResult' 134 135 export function samplingTransferInvoke(request: SamplingRequest): Promise<SamplingResponse> { 136 return new Promise<SamplingResponse>((resolve) => { 137 if (!mainWindow || mainWindow.isDestroyed()) { 138 resolve(null) 139 return 140 } 141 142 const responseChannel = `${msgSamplingTransferResultChannel}-${uuidv4()}` 143 144 listenOnceForRendererResponse(responseChannel, resolve) 145 146 mainWindow.webContents.send('msgSamplingTransferInvoke', { 147 request, 148 responseChannel 149 } as IpcSamplingRequest) 150 }) 151 } 152 153 const msgElicitationTransferResultChannel = 'msgElicitationTransferResult' 154 155 export function elicitationTransferInvoke(request: ElicitRequest): Promise<ElicitResponse> { 156 return new Promise((resolve) => { 157 if (!mainWindow || mainWindow.isDestroyed()) { 158 resolve(null) 159 return 160 } 161 162 const responseChannel = `${msgElicitationTransferResultChannel}-${uuidv4()}` 163 164 listenOnceForRendererResponse(responseChannel, resolve) 165 166 mainWindow.webContents.send('msgElicitationTransferInvoke', { 167 request, 168 responseChannel 169 } as IpcElicitRequest) 170 }) 171 } 172 173 export async function commandSelectionInvoke(request: CommandRequest) { 174 if (Constants.IS_DEV_ENV) { 175 await mainWindow.loadURL(`${Constants.APP_INDEX_URL_DEV}#/chat`) 176 } else { 177 await mainWindow.loadFile(Constants.APP_INDEX_URL_PROD, { hash: 'chat' }) 178 } 179 180 showWindow(mainWindow) 181 182 if (!mainWindow || mainWindow.isDestroyed()) { 183 return 184 } 185 186 console.log(request) 187 188 mainWindow.webContents.send('msgCommandSelectionInvoke', { 189 request 190 } as IpcCommandRequest) 191 } 192 193 export async function mcpServersProcessCallback(callback: McpProgressCallbackObject) { 194 mainWindow.webContents.send('msgMcpServersWatch', { 195 callback 196 } as IpcMcpInitRequest) 197 }