mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
fix(electron): default Windows close to background
Remove the one-time close prompt and remembered-choice path so Windows close behavior is always explicit and predictable. New installations now minimize to the system tray by default, while users can still choose to quit from settings.
This commit is contained in:
1 parent
35880228b7
commit
3db40375b6
15 files changed
+44
-215
No files matched your search
@@ -12,14 +12,8 @@ export default {
|
||||
error: 'Error',
|
||||
},
|
||||
|
||||
windowClose: {
|
||||
title: 'Close ChatLab',
|
||||
message: 'What should happen after the main window closes?',
|
||||
detail: 'Run in background keeps ChatLab available from the system tray.',
|
||||
background: 'Run in Background',
|
||||
windowsTray: {
|
||||
quitApp: 'Quit ChatLab',
|
||||
cancel: 'Cancel',
|
||||
remember: 'Remember my choice',
|
||||
showApp: 'Show ChatLab',
|
||||
},
|
||||
|
||||
|
||||
@@ -12,14 +12,8 @@ export default {
|
||||
error: 'エラー',
|
||||
},
|
||||
|
||||
windowClose: {
|
||||
title: 'ChatLab を閉じる',
|
||||
message: 'メインウィンドウを閉じた後の動作を選択してください。',
|
||||
detail: 'バックグラウンドで実行すると、ChatLab はシステムトレイから開けます。',
|
||||
background: 'バックグラウンドで実行',
|
||||
windowsTray: {
|
||||
quitApp: 'ChatLab を終了',
|
||||
cancel: 'キャンセル',
|
||||
remember: '選択を記憶する',
|
||||
showApp: 'ChatLab を表示',
|
||||
},
|
||||
|
||||
|
||||
@@ -11,14 +11,8 @@ export default {
|
||||
error: '错误',
|
||||
},
|
||||
|
||||
windowClose: {
|
||||
title: '关闭 ChatLab',
|
||||
message: '关闭主窗口后要执行什么操作?',
|
||||
detail: '选择后台运行后,ChatLab 会保留在系统托盘中。',
|
||||
background: '后台运行',
|
||||
windowsTray: {
|
||||
quitApp: '退出应用',
|
||||
cancel: '取消',
|
||||
remember: '记住我的选择',
|
||||
showApp: '显示 ChatLab',
|
||||
},
|
||||
|
||||
|
||||
@@ -12,14 +12,8 @@ export default {
|
||||
error: '錯誤',
|
||||
},
|
||||
|
||||
windowClose: {
|
||||
title: '關閉 ChatLab',
|
||||
message: '關閉主視窗後要執行什麼操作?',
|
||||
detail: '選擇背景執行後,ChatLab 會保留在系統匣中。',
|
||||
background: '背景執行',
|
||||
windowsTray: {
|
||||
quitApp: '結束應用程式',
|
||||
cancel: '取消',
|
||||
remember: '記住我的選擇',
|
||||
showApp: '顯示 ChatLab',
|
||||
},
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ export function registerWindowHandlers(ctx: IpcContext): void {
|
||||
})
|
||||
|
||||
ipcMain.handle('app:setDesktopCloseBehavior', (_, behavior: DesktopCloseBehavior) => {
|
||||
if (behavior !== 'ask' && behavior !== 'background' && behavior !== 'quit') {
|
||||
if (behavior !== 'background' && behavior !== 'quit') {
|
||||
return { success: false, error: 'Unsupported desktop close behavior' }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,83 +1,22 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, it } from 'node:test'
|
||||
import test from 'node:test'
|
||||
import type { DesktopCloseBehavior } from '@openchatlab/shared-types'
|
||||
import { WindowsCloseController } from './close-behavior'
|
||||
import { applyWindowsCloseBehavior } from './close-behavior'
|
||||
|
||||
interface HarnessOptions {
|
||||
preference?: DesktopCloseBehavior
|
||||
promptResult?: { action: 'background' | 'quit' | 'cancel'; remember: boolean }
|
||||
}
|
||||
|
||||
function createHarness(options: HarnessOptions = {}) {
|
||||
function applyBehavior(preference: DesktopCloseBehavior): string[] {
|
||||
const calls: string[] = []
|
||||
let resolvePrompt: (() => void) | undefined
|
||||
const promptGate = new Promise<void>((resolve) => {
|
||||
resolvePrompt = resolve
|
||||
})
|
||||
let waitForPrompt = false
|
||||
|
||||
const controller = new WindowsCloseController({
|
||||
readPreference: () => options.preference ?? 'ask',
|
||||
savePreference: (preference) => calls.push(`save:${preference}`),
|
||||
prompt: async () => {
|
||||
calls.push('prompt')
|
||||
if (waitForPrompt) await promptGate
|
||||
return options.promptResult ?? { action: 'cancel', remember: false }
|
||||
},
|
||||
applyWindowsCloseBehavior({
|
||||
readPreference: () => preference,
|
||||
enterBackground: () => calls.push('background'),
|
||||
quit: () => calls.push('quit'),
|
||||
onError: (error) => calls.push(`error:${String(error)}`),
|
||||
})
|
||||
|
||||
return {
|
||||
calls,
|
||||
controller,
|
||||
holdPrompt: () => {
|
||||
waitForPrompt = true
|
||||
},
|
||||
releasePrompt: () => resolvePrompt?.(),
|
||||
}
|
||||
return calls
|
||||
}
|
||||
|
||||
describe('WindowsCloseController', () => {
|
||||
it('applies remembered background and quit choices without prompting', async () => {
|
||||
for (const preference of ['background', 'quit'] as const) {
|
||||
const harness = createHarness({ preference })
|
||||
|
||||
await harness.controller.requestClose()
|
||||
|
||||
assert.deepEqual(harness.calls, [preference])
|
||||
}
|
||||
})
|
||||
|
||||
it('remembers the selected action only when the checkbox is enabled', async () => {
|
||||
const remembered = createHarness({ promptResult: { action: 'background', remember: true } })
|
||||
await remembered.controller.requestClose()
|
||||
assert.deepEqual(remembered.calls, ['prompt', 'save:background', 'background'])
|
||||
|
||||
const oneTime = createHarness({ promptResult: { action: 'quit', remember: false } })
|
||||
await oneTime.controller.requestClose()
|
||||
assert.deepEqual(oneTime.calls, ['prompt', 'quit'])
|
||||
})
|
||||
|
||||
it('keeps the window open when the prompt is cancelled', async () => {
|
||||
const harness = createHarness({ promptResult: { action: 'cancel', remember: true } })
|
||||
|
||||
await harness.controller.requestClose()
|
||||
|
||||
assert.deepEqual(harness.calls, ['prompt'])
|
||||
})
|
||||
|
||||
it('coalesces repeated close requests while the prompt is open', async () => {
|
||||
const harness = createHarness({ promptResult: { action: 'background', remember: false } })
|
||||
harness.holdPrompt()
|
||||
|
||||
const first = harness.controller.requestClose()
|
||||
const second = harness.controller.requestClose()
|
||||
assert.deepEqual(harness.calls, ['prompt'])
|
||||
|
||||
harness.releasePrompt()
|
||||
await Promise.all([first, second])
|
||||
assert.deepEqual(harness.calls, ['prompt', 'background'])
|
||||
})
|
||||
test('applies the configured Windows close behavior', () => {
|
||||
assert.deepEqual(applyBehavior('background'), ['background'])
|
||||
assert.deepEqual(applyBehavior('quit'), ['quit'])
|
||||
})
|
||||
@@ -1,82 +1,20 @@
|
||||
import type { DesktopCloseBehavior } from '@openchatlab/shared-types'
|
||||
|
||||
export type WindowsCloseAction = Exclude<DesktopCloseBehavior, 'ask'> | 'cancel'
|
||||
|
||||
export interface WindowsClosePromptResult {
|
||||
action: WindowsCloseAction
|
||||
remember: boolean
|
||||
}
|
||||
|
||||
interface WindowsCloseControllerDependencies {
|
||||
interface WindowsCloseBehaviorDependencies {
|
||||
readPreference: () => DesktopCloseBehavior
|
||||
savePreference: (preference: Exclude<DesktopCloseBehavior, 'ask'>) => void
|
||||
prompt: () => Promise<WindowsClosePromptResult>
|
||||
enterBackground: () => void
|
||||
quit: () => void
|
||||
onError: (error: unknown) => void
|
||||
}
|
||||
|
||||
export class WindowsCloseController {
|
||||
private handling = false
|
||||
|
||||
constructor(private readonly dependencies: WindowsCloseControllerDependencies) {}
|
||||
|
||||
async requestClose(): Promise<void> {
|
||||
if (this.handling) return
|
||||
this.handling = true
|
||||
|
||||
try {
|
||||
const preference = this.readPreference()
|
||||
const result = preference === 'ask' ? await this.prompt() : { action: preference, remember: false }
|
||||
|
||||
if (result.action === 'cancel') return
|
||||
|
||||
if (result.remember) {
|
||||
try {
|
||||
this.savePreference(result.action)
|
||||
} catch (error) {
|
||||
this.onError(error)
|
||||
}
|
||||
}
|
||||
|
||||
if (result.action === 'background') {
|
||||
this.enterBackground()
|
||||
} else {
|
||||
this.quit()
|
||||
}
|
||||
} catch (error) {
|
||||
this.onError(error)
|
||||
} finally {
|
||||
this.handling = false
|
||||
export function applyWindowsCloseBehavior(dependencies: WindowsCloseBehaviorDependencies): void {
|
||||
try {
|
||||
if (dependencies.readPreference() === 'background') {
|
||||
dependencies.enterBackground()
|
||||
} else {
|
||||
dependencies.quit()
|
||||
}
|
||||
}
|
||||
|
||||
private readPreference(): DesktopCloseBehavior {
|
||||
try {
|
||||
return this.dependencies.readPreference()
|
||||
} catch (error) {
|
||||
this.dependencies.onError(error)
|
||||
return 'ask'
|
||||
}
|
||||
}
|
||||
|
||||
private prompt(): Promise<WindowsClosePromptResult> {
|
||||
return this.dependencies.prompt()
|
||||
}
|
||||
|
||||
private savePreference(preference: Exclude<DesktopCloseBehavior, 'ask'>): void {
|
||||
this.dependencies.savePreference(preference)
|
||||
}
|
||||
|
||||
private enterBackground(): void {
|
||||
this.dependencies.enterBackground()
|
||||
}
|
||||
|
||||
private quit(): void {
|
||||
this.dependencies.quit()
|
||||
}
|
||||
|
||||
private onError(error: unknown): void {
|
||||
this.dependencies.onError(error)
|
||||
} catch (error) {
|
||||
dependencies.onError(error)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import { app, shell, BrowserWindow, nativeTheme, dialog } from 'electron'
|
||||
import { app, shell, BrowserWindow, nativeTheme } from 'electron'
|
||||
import { is, platform } from '@electron-toolkit/utils'
|
||||
import { loadConfig, setConfigField } from '@openchatlab/config'
|
||||
import { loadConfig } from '@openchatlab/config'
|
||||
import { applyCurrentTitleBarOverlay, getTitleBarOverlayOptions, resetCurrentTitleBarOverlayColor } from './titlebar'
|
||||
import { WindowsCloseController } from './close-behavior'
|
||||
import { applyWindowsCloseBehavior } from './close-behavior'
|
||||
import { ensureWindowsTray } from './windows-tray'
|
||||
import { t } from '../i18n'
|
||||
import { logger } from '../logger'
|
||||
|
||||
type AppWithQuitFlag = typeof app & { isQuiting?: boolean }
|
||||
@@ -85,31 +84,9 @@ export function requestAppQuit(): void {
|
||||
app.quit()
|
||||
}
|
||||
|
||||
function createWindowsCloseController(win: BrowserWindow): WindowsCloseController {
|
||||
return new WindowsCloseController({
|
||||
function handleWindowsClose(win: BrowserWindow): void {
|
||||
applyWindowsCloseBehavior({
|
||||
readPreference: () => loadConfig().desktop.close_behavior,
|
||||
savePreference: (preference) => {
|
||||
setConfigField('desktop.close_behavior', preference)
|
||||
},
|
||||
prompt: async () => {
|
||||
const result = await dialog.showMessageBox(win, {
|
||||
type: 'question',
|
||||
title: t('windowClose.title'),
|
||||
message: t('windowClose.message'),
|
||||
detail: t('windowClose.detail'),
|
||||
buttons: [t('windowClose.background'), t('windowClose.quitApp'), t('windowClose.cancel')],
|
||||
defaultId: 0,
|
||||
cancelId: 2,
|
||||
noLink: true,
|
||||
checkboxLabel: t('windowClose.remember'),
|
||||
checkboxChecked: false,
|
||||
})
|
||||
|
||||
return {
|
||||
action: result.response === 0 ? 'background' : result.response === 1 ? 'quit' : 'cancel',
|
||||
remember: result.checkboxChecked,
|
||||
}
|
||||
},
|
||||
enterBackground: () => {
|
||||
ensureWindowsTray(win, requestAppQuit)
|
||||
win.hide()
|
||||
@@ -122,8 +99,6 @@ function createWindowsCloseController(win: BrowserWindow): WindowsCloseControlle
|
||||
}
|
||||
|
||||
function registerMainWindowEvents(win: BrowserWindow): void {
|
||||
const windowsCloseController = platform.isWindows ? createWindowsCloseController(win) : null
|
||||
|
||||
win.webContents.on('did-finish-load', () => {
|
||||
setTimeout(() => {
|
||||
currentMainWindow?.webContents.send('app-started')
|
||||
@@ -153,7 +128,7 @@ function registerMainWindowEvents(win: BrowserWindow): void {
|
||||
|
||||
if (platform.isWindows && !appWithQuitFlag.isQuiting) {
|
||||
event.preventDefault()
|
||||
void windowsCloseController?.requestClose()
|
||||
handleWindowsClose(win)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -18,9 +18,9 @@ export function ensureWindowsTray(win: BrowserWindow, onQuit: () => void): void
|
||||
|
||||
tray.setContextMenu(
|
||||
Menu.buildFromTemplate([
|
||||
{ label: t('windowClose.showApp'), click: restoreMainWindow },
|
||||
{ label: t('windowsTray.showApp'), click: restoreMainWindow },
|
||||
{ type: 'separator' },
|
||||
{ label: t('windowClose.quitApp'), click: () => quitHandler?.() },
|
||||
{ label: t('windowsTray.quitApp'), click: () => quitHandler?.() },
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,19 +18,20 @@ test('rejects unknown session landing tabs', () => {
|
||||
assert.throws(() => configSchema.parse({ ui: { default_session_tab: 'unknown' } }))
|
||||
})
|
||||
|
||||
test('defaults Windows close behavior to asking the user', () => {
|
||||
test('defaults Windows close behavior to running in the background', () => {
|
||||
const config = configSchema.parse({})
|
||||
|
||||
assert.equal(config.desktop.close_behavior, 'ask')
|
||||
assert.equal(config.desktop.close_behavior, 'background')
|
||||
})
|
||||
|
||||
test('accepts supported Windows close behaviors and rejects invalid values', () => {
|
||||
for (const closeBehavior of ['ask', 'background', 'quit']) {
|
||||
for (const closeBehavior of ['background', 'quit']) {
|
||||
assert.equal(
|
||||
configSchema.parse({ desktop: { close_behavior: closeBehavior } }).desktop.close_behavior,
|
||||
closeBehavior
|
||||
)
|
||||
}
|
||||
|
||||
assert.throws(() => configSchema.parse({ desktop: { close_behavior: 'ask' } }))
|
||||
assert.throws(() => configSchema.parse({ desktop: { close_behavior: 'hide-forever' } }))
|
||||
})
|
||||
@@ -47,7 +47,7 @@ export const cliConfigSchema = z.object({
|
||||
})
|
||||
|
||||
export const desktopConfigSchema = z.object({
|
||||
close_behavior: z.enum(['ask', 'background', 'quit']).default('ask'),
|
||||
close_behavior: z.enum(['background', 'quit']).default('background'),
|
||||
})
|
||||
|
||||
export const configSchema = z.object({
|
||||
|
||||
@@ -666,7 +666,7 @@ export interface UiConfig {
|
||||
summary_strategy?: 'brief' | 'standard'
|
||||
}
|
||||
|
||||
export type DesktopCloseBehavior = 'ask' | 'background' | 'quit'
|
||||
export type DesktopCloseBehavior = 'background' | 'quit'
|
||||
|
||||
// ==================== 匿名使用统计 ====================
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ const openAtLogin = ref(false)
|
||||
const isPackaged = ref(true)
|
||||
const isWindowsDesktop =
|
||||
IS_ELECTRON && typeof navigator !== 'undefined' && navigator.platform.toLowerCase().includes('win')
|
||||
const desktopCloseBehavior = ref<DesktopCloseBehavior>('ask')
|
||||
let savedDesktopCloseBehavior: DesktopCloseBehavior = 'ask'
|
||||
const desktopCloseBehavior = ref<DesktopCloseBehavior>('background')
|
||||
let savedDesktopCloseBehavior: DesktopCloseBehavior = 'background'
|
||||
|
||||
onMounted(async () => {
|
||||
if (!IS_ELECTRON) {
|
||||
@@ -55,7 +55,7 @@ onMounted(async () => {
|
||||
savedDesktopCloseBehavior = await usePlatformService().getDesktopCloseBehavior()
|
||||
desktopCloseBehavior.value = savedDesktopCloseBehavior
|
||||
} catch {
|
||||
desktopCloseBehavior.value = 'ask'
|
||||
desktopCloseBehavior.value = 'background'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -70,7 +70,7 @@ async function handleAutoLaunchChange(enabled: boolean) {
|
||||
}
|
||||
|
||||
async function handleDesktopCloseBehaviorChange(value: string | number) {
|
||||
if (value !== 'ask' && value !== 'background' && value !== 'quit') return
|
||||
if (value !== 'background' && value !== 'quit') return
|
||||
|
||||
const nextBehavior: DesktopCloseBehavior = value
|
||||
desktopCloseBehavior.value = nextBehavior
|
||||
@@ -183,7 +183,7 @@ const desktopCloseBehaviorOptions = computed(() => [
|
||||
</div>
|
||||
<div class="w-64">
|
||||
<UTabs
|
||||
:model-value="desktopCloseBehavior === 'ask' ? undefined : desktopCloseBehavior"
|
||||
:model-value="desktopCloseBehavior"
|
||||
size="sm"
|
||||
class="gap-0"
|
||||
:items="desktopCloseBehaviorOptions"
|
||||
|
||||
@@ -42,7 +42,7 @@ export class BrowserPlatformAdapter implements PlatformAdapter {
|
||||
}
|
||||
|
||||
async getDesktopCloseBehavior(): Promise<DesktopCloseBehavior> {
|
||||
return 'ask'
|
||||
return 'background'
|
||||
}
|
||||
|
||||
async setDesktopCloseBehavior(_behavior: DesktopCloseBehavior): Promise<{ success: boolean; error?: string }> {
|
||||
|
||||
@@ -49,7 +49,7 @@ export class CliWebPlatformAdapter implements PlatformAdapter {
|
||||
}
|
||||
|
||||
async getDesktopCloseBehavior(): Promise<DesktopCloseBehavior> {
|
||||
return 'ask'
|
||||
return 'background'
|
||||
}
|
||||
|
||||
async setDesktopCloseBehavior(_behavior: DesktopCloseBehavior): Promise<{ success: boolean; error?: string }> {
|
||||
|
||||
Reference in new issue
Block a user