/ src / main / index.ts
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  }