Skip to content

Commit ae47f70

Browse files
authored
refactor(main): extract IPC handlers into dedicated modules (#1720)
* refactor(main): extract IPC handlers into dedicated modules * test: add basic test for indexes
1 parent fe9f568 commit ae47f70

20 files changed

Lines changed: 714 additions & 561 deletions
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest'
2+
3+
const mocks = vi.hoisted(() => ({
4+
registerApp: vi.fn(),
5+
registerAutoUpdate: vi.fn(),
6+
registerChat: vi.fn(),
7+
registerCli: vi.fn(),
8+
registerDarkMode: vi.fn(),
9+
registerDialogs: vi.fn(),
10+
registerFeatureFlags: vi.fn(),
11+
registerTelemetry: vi.fn(),
12+
registerToolhive: vi.fn(),
13+
registerUtils: vi.fn(),
14+
registerWindow: vi.fn(),
15+
}))
16+
17+
vi.mock('../app', () => ({ register: mocks.registerApp }))
18+
vi.mock('../auto-update', () => ({ register: mocks.registerAutoUpdate }))
19+
vi.mock('../chat', () => ({ register: mocks.registerChat }))
20+
vi.mock('../cli', () => ({ register: mocks.registerCli }))
21+
vi.mock('../dark-mode', () => ({ register: mocks.registerDarkMode }))
22+
vi.mock('../dialogs', () => ({ register: mocks.registerDialogs }))
23+
vi.mock('../feature-flags', () => ({ register: mocks.registerFeatureFlags }))
24+
vi.mock('../telemetry', () => ({ register: mocks.registerTelemetry }))
25+
vi.mock('../toolhive', () => ({ register: mocks.registerToolhive }))
26+
vi.mock('../utils', () => ({ register: mocks.registerUtils }))
27+
vi.mock('../window', () => ({ register: mocks.registerWindow }))
28+
29+
import { registerAllHandlers } from '../index'
30+
31+
describe('registerAllHandlers', () => {
32+
beforeEach(() => {
33+
vi.clearAllMocks()
34+
})
35+
36+
it('calls every handler registration function exactly once', () => {
37+
registerAllHandlers()
38+
39+
expect(mocks.registerApp).toHaveBeenCalledOnce()
40+
expect(mocks.registerAutoUpdate).toHaveBeenCalledOnce()
41+
expect(mocks.registerChat).toHaveBeenCalledOnce()
42+
expect(mocks.registerCli).toHaveBeenCalledOnce()
43+
expect(mocks.registerDarkMode).toHaveBeenCalledOnce()
44+
expect(mocks.registerDialogs).toHaveBeenCalledOnce()
45+
expect(mocks.registerFeatureFlags).toHaveBeenCalledOnce()
46+
expect(mocks.registerTelemetry).toHaveBeenCalledOnce()
47+
expect(mocks.registerToolhive).toHaveBeenCalledOnce()
48+
expect(mocks.registerUtils).toHaveBeenCalledOnce()
49+
expect(mocks.registerWindow).toHaveBeenCalledOnce()
50+
})
51+
})

main/src/ipc-handlers/app.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { app, ipcMain } from 'electron'
2+
import { existsSync, readFile } from 'node:fs'
3+
import path from 'node:path'
4+
import { blockQuit } from '../app-events'
5+
import { setAutoLaunch, getAutoLaunchStatus } from '../auto-launch'
6+
import { showInDock } from '../dock-utils'
7+
import log from '../logger'
8+
import { showMainWindow, hideMainWindow } from '../main-window'
9+
import { createApplicationMenu } from '../menu'
10+
import {
11+
getSkipQuitConfirmation,
12+
setSkipQuitConfirmation,
13+
} from '../quit-confirmation'
14+
import { updateTrayStatus } from '../system-tray'
15+
import { isToolhiveRunning } from '../toolhive-manager'
16+
17+
export function register() {
18+
ipcMain.handle('get-auto-launch-status', () => getAutoLaunchStatus())
19+
20+
ipcMain.handle('set-auto-launch', (_event, enabled: boolean) => {
21+
setAutoLaunch(enabled)
22+
updateTrayStatus(isToolhiveRunning())
23+
createApplicationMenu()
24+
return getAutoLaunchStatus()
25+
})
26+
27+
ipcMain.handle('show-app', async () => {
28+
try {
29+
showInDock()
30+
await showMainWindow()
31+
} catch (error) {
32+
log.error('Failed to show app:', error)
33+
}
34+
})
35+
36+
ipcMain.handle('hide-app', () => {
37+
try {
38+
hideMainWindow()
39+
} catch (error) {
40+
log.error('Failed to hide app:', error)
41+
}
42+
})
43+
44+
ipcMain.handle('quit-app', () => {
45+
blockQuit('before-quit')
46+
})
47+
48+
ipcMain.handle('get-skip-quit-confirmation', () => getSkipQuitConfirmation())
49+
ipcMain.handle('set-skip-quit-confirmation', (_e, skip: boolean) =>
50+
setSkipQuitConfirmation(skip)
51+
)
52+
53+
ipcMain.handle(
54+
'get-main-log-content',
55+
async (): Promise<string | undefined> => {
56+
try {
57+
const logPath = path.join(app.getPath('logs'), 'main.log')
58+
if (!existsSync(logPath)) {
59+
log.warn(`Log file does not exist: ${logPath}`)
60+
return
61+
}
62+
63+
const content = await new Promise<string>((resolve, reject) => {
64+
readFile(logPath, 'utf8', (err, data) => {
65+
if (err) reject(err)
66+
else resolve(data)
67+
})
68+
})
69+
70+
return content
71+
} catch (error) {
72+
log.error('Failed to read log file:', error)
73+
return
74+
}
75+
}
76+
)
77+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { ipcMain } from 'electron'
2+
import {
3+
getIsAutoUpdateEnabled,
4+
getLatestAvailableVersion,
5+
getUpdateState,
6+
manualUpdate,
7+
setAutoUpdateEnabled,
8+
} from '../auto-update'
9+
import log from '../logger'
10+
11+
export function register() {
12+
ipcMain.handle('auto-update:set', async (_, enabled: boolean) => {
13+
setAutoUpdateEnabled(enabled)
14+
return enabled
15+
})
16+
17+
ipcMain.handle('auto-update:get', () => {
18+
return getIsAutoUpdateEnabled()
19+
})
20+
21+
ipcMain.handle('manual-update', async () => {
22+
log.info('[update] triggered manual update')
23+
manualUpdate()
24+
})
25+
26+
ipcMain.handle('get-app-version', async () => {
27+
const versionInfo = await getLatestAvailableVersion()
28+
return versionInfo
29+
})
30+
31+
ipcMain.handle('get-update-state', async () => {
32+
return getUpdateState()
33+
})
34+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest'
2+
3+
const mocks = vi.hoisted(() => ({
4+
registerMcpTools: vi.fn(),
5+
registerProviders: vi.fn(),
6+
registerSettings: vi.fn(),
7+
registerStreaming: vi.fn(),
8+
registerThreads: vi.fn(),
9+
}))
10+
11+
vi.mock('../mcp-tools', () => ({ register: mocks.registerMcpTools }))
12+
vi.mock('../providers', () => ({ register: mocks.registerProviders }))
13+
vi.mock('../settings', () => ({ register: mocks.registerSettings }))
14+
vi.mock('../streaming', () => ({ register: mocks.registerStreaming }))
15+
vi.mock('../threads', () => ({ register: mocks.registerThreads }))
16+
17+
import { register } from '../index'
18+
19+
describe('chat register', () => {
20+
beforeEach(() => {
21+
vi.clearAllMocks()
22+
})
23+
24+
it('calls every chat handler registration function exactly once', () => {
25+
register()
26+
27+
expect(mocks.registerProviders).toHaveBeenCalledOnce()
28+
expect(mocks.registerStreaming).toHaveBeenCalledOnce()
29+
expect(mocks.registerSettings).toHaveBeenCalledOnce()
30+
expect(mocks.registerMcpTools).toHaveBeenCalledOnce()
31+
expect(mocks.registerThreads).toHaveBeenCalledOnce()
32+
})
33+
})
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { register as registerMcpTools } from './mcp-tools'
2+
import { register as registerProviders } from './providers'
3+
import { register as registerSettings } from './settings'
4+
import { register as registerStreaming } from './streaming'
5+
import { register as registerThreads } from './threads'
6+
7+
export function register() {
8+
registerProviders()
9+
registerStreaming()
10+
registerSettings()
11+
registerMcpTools()
12+
registerThreads()
13+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ipcMain } from 'electron'
2+
import { getMcpServerTools, getToolhiveMcpInfo } from '../../chat'
3+
import {
4+
getEnabledMcpTools,
5+
getEnabledMcpServersFromTools,
6+
saveEnabledMcpTools,
7+
} from '../../chat/settings-storage'
8+
9+
export function register() {
10+
ipcMain.handle('chat:get-mcp-server-tools', (_, serverName: string) =>
11+
getMcpServerTools(serverName)
12+
)
13+
ipcMain.handle('chat:get-enabled-mcp-tools', () => getEnabledMcpTools())
14+
ipcMain.handle('chat:get-enabled-mcp-servers-from-tools', () =>
15+
getEnabledMcpServersFromTools()
16+
)
17+
ipcMain.handle(
18+
'chat:save-enabled-mcp-tools',
19+
(_, serverName: string, enabledTools: string[]) =>
20+
saveEnabledMcpTools(serverName, enabledTools)
21+
)
22+
ipcMain.handle('chat:get-toolhive-mcp-info', () => getToolhiveMcpInfo())
23+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ipcMain } from 'electron'
2+
import {
3+
discoverToolSupportedModels,
4+
fetchProviderModelsHandler,
5+
getAllProvidersHandler,
6+
} from '../../chat/providers'
7+
8+
export function register() {
9+
ipcMain.handle(
10+
'chat:fetch-provider-models',
11+
(_, providerId: string, tempCredential?: string) =>
12+
fetchProviderModelsHandler(providerId, tempCredential)
13+
)
14+
15+
ipcMain.handle('chat:get-providers', () => getAllProvidersHandler())
16+
17+
ipcMain.handle('chat:discover-models', () => discoverToolSupportedModels())
18+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { ipcMain } from 'electron'
2+
import {
3+
getChatSettings,
4+
clearChatSettings,
5+
getSelectedModel,
6+
saveSelectedModel,
7+
handleSaveSettings,
8+
} from '../../chat/settings-storage'
9+
10+
export function register() {
11+
ipcMain.handle('chat:get-settings', (_, providerId: string) =>
12+
getChatSettings(providerId as Parameters<typeof getChatSettings>[0])
13+
)
14+
ipcMain.handle(
15+
'chat:save-settings',
16+
(
17+
_,
18+
providerId: string,
19+
settings:
20+
| { apiKey: string; enabledTools: string[] }
21+
| { endpointURL: string; enabledTools: string[] }
22+
) => handleSaveSettings(providerId, settings)
23+
)
24+
ipcMain.handle('chat:clear-settings', (_, providerId?: string) =>
25+
clearChatSettings(providerId as Parameters<typeof clearChatSettings>[0])
26+
)
27+
28+
ipcMain.handle('chat:get-selected-model', () => getSelectedModel())
29+
ipcMain.handle(
30+
'chat:save-selected-model',
31+
(_, provider: string, model: string) => saveSelectedModel(provider, model)
32+
)
33+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ipcMain } from 'electron'
2+
import { type ChatRequest, handleChatStreamRealtime } from '../../chat'
3+
4+
export function register() {
5+
ipcMain.handle('chat:stream', async (event, request: ChatRequest) => {
6+
const streamId = `stream-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
7+
handleChatStreamRealtime(request, streamId, event.sender)
8+
return { streamId }
9+
})
10+
}

0 commit comments

Comments
 (0)