Skip to content

Commit d88255b

Browse files
authored
fix: quit will always trigger graceful-exit (#474)
* fix: quit will always trigger graceful-exit * fix: hide confirmation dialog when close app by traffic lights * fix: remove confirm in window-control * fix: system tray show app * refactor: after review * fix: leftover
1 parent 8ea7454 commit d88255b

File tree

5 files changed

+53
-30
lines changed

5 files changed

+53
-30
lines changed

main/src/graceful-exit.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import { createClient } from '../../renderer/src/common/api/generated/client'
66
import type { WorkloadsWorkload } from '../../renderer/src/common/api/generated/types.gen'
77
import Store from 'electron-store'
88
import log from './logger'
9-
10-
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
9+
import { delay } from './util'
1110

1211
// Create a store instance for tracking shutdown servers
1312
const shutdownStore = new Store({

main/src/main.ts

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
binPath,
3333
} from './toolhive-manager'
3434
import log from './logger'
35-
import { getAppVersion } from './util'
35+
import { delay, getAppVersion, pollWindowReady } from './util'
3636

3737
import Store from 'electron-store'
3838

@@ -101,10 +101,28 @@ export async function blockQuit(source: string, event?: Electron.Event) {
101101
event.preventDefault()
102102
}
103103

104-
// Only send graceful-exit message if mainWindow is still valid
105104
try {
106-
if (mainWindow && !mainWindow.isDestroyed()) {
105+
if (!mainWindow || mainWindow.isDestroyed()) {
106+
log.info('MainWindow destroyed, recreating for graceful shutdown...')
107+
mainWindow = createWindow()
108+
}
109+
110+
if (mainWindow) {
111+
log.info('Showing window for graceful shutdown...')
112+
113+
if (mainWindow.isMinimized()) {
114+
mainWindow.restore()
115+
}
116+
117+
mainWindow.show()
118+
mainWindow.focus()
119+
120+
await pollWindowReady(mainWindow)
121+
107122
mainWindow.webContents.send('graceful-exit')
123+
124+
// Give renderer time to navigate to shutdown page
125+
await delay(500)
108126
}
109127
} catch (err) {
110128
log.error('Failed to send graceful-exit message: ', err)
@@ -194,18 +212,16 @@ function createWindow() {
194212
...getPlatformSpecificWindowOptions(),
195213
})
196214

197-
// Windows: minimise-to-tray instead of close
198-
if (process.platform === 'win32') {
199-
mainWindow.on('minimize', () => {
200-
if (shouldStartHidden || tray) mainWindow.hide()
201-
})
202-
mainWindow.on('close', (event) => {
203-
if (!isQuitting && tray) {
204-
event.preventDefault()
205-
mainWindow.hide()
206-
}
207-
})
208-
}
215+
// minimise-to-tray instead of close
216+
mainWindow.on('minimize', () => {
217+
if (shouldStartHidden || tray) mainWindow.hide()
218+
})
219+
mainWindow.on('close', (event) => {
220+
if (!isQuitting && tray) {
221+
event.preventDefault()
222+
mainWindow.hide()
223+
}
224+
})
209225

210226
// External links → default browser
211227
mainWindow.webContents.setWindowOpenHandler(({ url }) => {

main/src/system-tray.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,17 +202,14 @@ const createMenuTemplate = (currentTray: Tray, toolHiveIsRunning: boolean) => [
202202

203203
const createClickHandler = () => {
204204
let lastClickTime = 0
205-
let lastWindowState = false
206205

207206
const isRapidClick = (now: number) => now - lastClickTime < 300
208207

209208
const toggleWindow = (window: BrowserWindow) => {
210-
if (lastWindowState) {
209+
if (window.isVisible() && !window.isMinimized()) {
211210
hideWindow(window)
212-
lastWindowState = false
213211
} else {
214212
showWindowWithFocus(window)
215-
lastWindowState = true
216213
}
217214
}
218215

main/src/util.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { app } from 'electron'
1+
import { app, BrowserWindow } from 'electron'
22
import { execSync } from 'node:child_process'
3+
import log from './logger'
34

45
function getVersionFromGit(): string {
56
try {
@@ -24,10 +25,25 @@ function getVersionFromGit(): string {
2425
}
2526
}
2627

28+
export async function delay(ms: number) {
29+
return new Promise((resolve) => setTimeout(resolve, ms))
30+
}
31+
2732
export function getAppVersion(): string {
2833
if (process.env.SENTRY_RELEASE) {
2934
return process.env.SENTRY_RELEASE
3035
}
3136

3237
return getVersionFromGit()
3338
}
39+
40+
export async function pollWindowReady(window: BrowserWindow): Promise<void> {
41+
if (window?.isVisible() && !window.webContents.isLoading()) {
42+
log.info('Window is ready and visible')
43+
return
44+
}
45+
46+
log.info('Window not ready yet, waiting...')
47+
await delay(100)
48+
return pollWindowReady(window)
49+
}

renderer/src/common/components/layout/top-nav/window-controls.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { Button } from '../../ui/button'
22
import { Minus, Square, X } from 'lucide-react'
33
import { useState, useEffect } from 'react'
4-
import { useConfirmQuit } from '@/common/hooks/use-confirm-quit'
54

65
export function WindowControls() {
76
const [isMaximized, setIsMaximized] = useState(false)
8-
const confirmQuit = useConfirmQuit()
97

108
useEffect(() => {
119
// Check initial maximized state
@@ -23,10 +21,7 @@ export function WindowControls() {
2321
}
2422

2523
const handleClose = async () => {
26-
const confirmed = await confirmQuit()
27-
if (confirmed) {
28-
await window.electronAPI.windowControls.close()
29-
}
24+
await window.electronAPI.windowControls.close()
3025
}
3126

3227
// Only show window controls on Windows and Linux (not macOS)
@@ -52,8 +47,8 @@ export function WindowControls() {
5247
>
5348
{isMaximized ? (
5449
<div className="relative size-4">
55-
<div className="absolute inset-0 h-3 w-3 border border-current" />
56-
<div className="bg-background absolute top-1 left-1 h-3 w-3 border border-current" />
50+
<div className="absolute inset-0 size-3 border border-current" />
51+
<div className="bg-background absolute top-1 left-1 size-3 border border-current" />
5752
</div>
5853
) : (
5954
<Square className="size-4" />

0 commit comments

Comments
 (0)