Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions apps/buddy/electron/main/__tests__/window.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,38 @@ vi.mock('electron', () => ({
}))

describe('createDesktopWindow lifecycle', () => {
it('requests application quit instead of hiding when tray minimization is disabled', () => {
const onCloseToQuit = vi.fn()
const onHidden = vi.fn()
createDesktopWindow({
appName: 'Lexora Buddy Dev',
iconPath: '/tmp/icon.png',
isQuitting: () => false,
minimizeToTrayOnClose: () => false,
onCloseToQuit,
onHidden,
rendererUrl: null,
})
const window = electron.FakeBrowserWindow.instances.at(-1)!
const event = { preventDefault: vi.fn() }

window.emit('close', event)

expect(event.preventDefault).toHaveBeenCalledOnce()
expect(onCloseToQuit).toHaveBeenCalledOnce()
expect(onHidden).not.toHaveBeenCalled()
expect(window.hide).not.toHaveBeenCalled()
})

it('hides to tray on ordinary close and lets a committed quit close the window', () => {
let isQuitting = false
const onHidden = vi.fn()
createDesktopWindow({
appName: 'Lexora Buddy Dev',
iconPath: '/tmp/icon.png',
isQuitting: () => isQuitting,
minimizeToTrayOnClose: () => true,
onCloseToQuit: vi.fn(),
onHidden,
rendererUrl: null,
})
Expand Down
2 changes: 2 additions & 0 deletions apps/buddy/electron/main/app/DesktopApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ class DesktopApplication {
return this.#windows.initialize({
onWindowCreated: window => this.#browser.bindWindow(window),
isQuitting: () => this.#quit.quitting,
minimizeToTrayOnClose: () => this.#runtime.config?.desktop.minimizeToTrayOnClose ?? true,
onCloseToQuit: () => this.#requestQuit(),
onHidden: () => { void showBackgroundCloseNotice(this.#runtime.configStore) },
})
}, ['desktop.integrations'])
Expand Down
4 changes: 4 additions & 0 deletions apps/buddy/electron/main/app/DesktopWindowHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { observeRendererDiagnostics } from './desktopProcessDiagnostics'

interface WindowBindings {
isQuitting: () => boolean
minimizeToTrayOnClose: () => boolean
onCloseToQuit: () => void
onHidden: () => void
onWindowCreated: (window: BrowserWindow) => void
}
Expand Down Expand Up @@ -46,6 +48,8 @@ export class DesktopWindowHost {
appName: environment.paths.appName,
iconPath: environment.desktopIconPath,
isQuitting: bindings.isQuitting,
minimizeToTrayOnClose: bindings.minimizeToTrayOnClose,
onCloseToQuit: bindings.onCloseToQuit,
onHidden() {
if (!handle.window.webContents.isDestroyed())
handle.window.webContents.send(DESKTOP_IPC_CHANNELS.appHidden)
Expand Down
4 changes: 4 additions & 0 deletions apps/buddy/electron/main/config/LexoraConfigStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const desktopConfigSchema = z.object({
}).passthrough().prefault({}),
context_panel_mode: z.enum(['task', 'independent']).default('task'),
context_panel_global: z.boolean().default(false),
minimize_to_tray_on_close: z.boolean().default(true),
keybindings: keybindingsSchema.default({}),
task_sidebar_pinned_items: z.array(taskSidebarPinnedItemSchema)
.max(500)
Expand Down Expand Up @@ -75,6 +76,7 @@ const desktopConfigSchema = z.object({
},
context_panel_mode: 'task',
context_panel_global: false,
minimize_to_tray_on_close: true,
keybindings: {},
task_sidebar_pinned_items: [],
task_sidebar: { collapsed: false, collapsed_sections: [], collapsed_spaces: [] },
Expand Down Expand Up @@ -231,6 +233,7 @@ function decodeConfig(value: unknown): LexoraConfig {
},
contextPanelMode: config.desktop.context_panel_mode,
contextPanelGlobal: config.desktop.context_panel_global,
minimizeToTrayOnClose: config.desktop.minimize_to_tray_on_close,
keybindings: config.desktop.keybindings,
taskSidebarPinnedItems: config.desktop.task_sidebar_pinned_items,
taskSidebar: {
Expand Down Expand Up @@ -280,6 +283,7 @@ function encodeConfig(config: LexoraConfig) {
},
context_panel_mode: config.desktop.contextPanelMode,
context_panel_global: config.desktop.contextPanelGlobal,
minimize_to_tray_on_close: config.desktop.minimizeToTrayOnClose,
keybindings: config.desktop.keybindings,
task_sidebar_pinned_items: config.desktop.taskSidebarPinnedItems,
task_sidebar: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ describe('lexoraConfigStore', () => {
const updated = await store.update({
desktop: {
theme: 'dark',
minimizeToTrayOnClose: false,
chat: { welcome: 'writing' },
},
pet: { alwaysOnTop: false, enabled: false, rememberPosition: false },
Expand All @@ -150,6 +151,7 @@ describe('lexoraConfigStore', () => {
expect(updated.desktop).toEqual({
contextPanelMode: 'task',
contextPanelGlobal: false,
minimizeToTrayOnClose: false,
keybindings: {},
backgroundCloseNoticeShown: false,
pluginAuthor: '',
Expand All @@ -176,6 +178,7 @@ describe('lexoraConfigStore', () => {
})
const content = await readFile(configPath, 'utf8')
expect(content).toContain('[desktop]')
expect(content).toContain('minimize_to_tray_on_close = false')
expect(content).toContain('[pet]')
expect(content).toContain('always_on_top = false')
expect(content).toContain('enabled = false')
Expand Down
11 changes: 9 additions & 2 deletions apps/buddy/electron/main/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export interface CreateDesktopWindowOptions {
appName: string
iconPath: string
isQuitting: () => boolean
minimizeToTrayOnClose: () => boolean
onCloseToQuit: () => void
onHidden?: () => void
onPlacementChanged?: (placement: DesktopWindowPlacement) => void
placement?: DesktopWindowPlacement | null
Expand Down Expand Up @@ -104,8 +106,13 @@ export function createDesktopWindow(options: CreateDesktopWindowOptions): Deskto
return

event.preventDefault()
window.hide()
options.onHidden?.()
if (options.minimizeToTrayOnClose()) {
window.hide()
options.onHidden?.()
}
else {
options.onCloseToQuit()
}
})

window.once('ready-to-show', () => {
Expand Down
1 change: 1 addition & 0 deletions apps/buddy/electron/shared/desktopApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export interface LexoraConfig {
chat: DesktopChatPreferences
contextPanelMode: DesktopContextPanelMode
contextPanelGlobal: boolean
minimizeToTrayOnClose: boolean
keybindings: Record<string, string>
taskSidebarPinnedItems: DesktopTaskPinnedItem[]
taskSidebar: DesktopTaskSidebarPreferences
Expand Down
1 change: 1 addition & 0 deletions apps/buddy/electron/shared/desktopApiSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const lexoraConfigPatchSchema: z.ZodType<LexoraConfigPatch> = z.object({
}).strict().optional(),
contextPanelMode: z.enum(['task', 'independent']).optional(),
contextPanelGlobal: z.boolean().optional(),
minimizeToTrayOnClose: z.boolean().optional(),
keybindings: keybindingsSchema.optional(),
taskSidebarPinnedItems: taskSidebarPinnedItemsSchema.optional(),
taskSidebar: taskSidebarPreferencesSchema.optional(),
Expand Down
3 changes: 3 additions & 0 deletions apps/buddy/src/i18n/locales/en-US/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ export default {
'desktop.browser.screenshotSaved': 'Screenshot saved',
'desktop.browser.screenshotCopied': 'Screenshot copied to clipboard',
'desktop.browser.screenshotFailed': 'Could not capture the screenshot. Retry or change the destination in browser settings.',
'desktop.settings.windowBehavior': 'Window behavior',
'desktop.settings.minimizeToTrayOnClose': 'Minimize to tray when closing the window',
'desktop.settings.minimizeToTrayOnCloseDescription': 'When enabled, the window hides and Lexora Buddy keeps running in the background. When disabled, closing the window quits the app.',
'desktop.settings.contextPanel': 'Resource panel',
'desktop.settings.contextPanelMode': 'Browsing mode',
'desktop.settings.contextPanelModeDescription': 'Task-linked selects resources when focusing a task. Independent keeps the current selection.',
Expand Down
3 changes: 3 additions & 0 deletions apps/buddy/src/i18n/locales/zh-CN/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ export default {
'desktop.browser.screenshotSaved': '截图已保存',
'desktop.browser.screenshotCopied': '截图已复制到剪贴板',
'desktop.browser.screenshotFailed': '截图失败,请重试或在浏览器设置中切换保存方式。',
'desktop.settings.windowBehavior': '窗口行为',
'desktop.settings.minimizeToTrayOnClose': '关闭窗口时最小化到托盘',
'desktop.settings.minimizeToTrayOnCloseDescription': '开启后窗口会隐藏,Lexora Buddy 仍在后台运行;关闭后点击关闭按钮会退出应用。',
'desktop.settings.contextPanel': '资源面板',
'desktop.settings.contextPanelMode': '浏览模式',
'desktop.settings.contextPanelModeDescription': '任务联动在聚焦任务时选择其资源,独立浏览保留当前选择。',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import { NSelect, NSpin, NSwitch, useMessage } from 'naive-ui'
import { computed, shallowRef, useId } from 'vue'
import { useBuddyI18n } from '@/i18n/buddyI18n'

type GeneralSettingField = 'language' | 'contextPanelMode' | 'contextPanelGlobal'
type GeneralSettingField = 'language' | 'contextPanelMode' | 'contextPanelGlobal' | 'minimizeToTrayOnClose'

const props = defineProps<ApplicationSettingsProps>()
const { languageOptions, t } = useBuddyI18n(() => props.language)
const message = useMessage()
const pendingFields = shallowRef<ReadonlySet<GeneralSettingField>>(new Set())
const globalPanelLabelId = useId()
const minimizeToTrayLabelId = useId()
const contextPanelModes = computed(() => [
{ label: t('desktop.settings.contextPanelTask'), value: 'task' },
{ label: t('desktop.settings.contextPanelIndependent'), value: 'independent' },
Expand Down Expand Up @@ -52,6 +53,29 @@ async function updateSetting(field: GeneralSettingField, patch: LexoraConfigPatc
</div>
</div>
</section>
<section class="desktop-general-settings__section">
<h2 class="desktop-general-settings__title">
{{ t('desktop.settings.windowBehavior') }}
</h2>
<div class="desktop-general-settings__group">
<div class="desktop-settings-row" data-testid="minimize-to-tray-setting">
<div class="desktop-settings-row__copy">
<strong :id="minimizeToTrayLabelId">{{ t('desktop.settings.minimizeToTrayOnClose') }}</strong>
<small>{{ t('desktop.settings.minimizeToTrayOnCloseDescription') }}</small>
</div>
<div class="desktop-settings-row__control desktop-settings-row__control--toggle">
<NSwitch
:aria-labelledby="minimizeToTrayLabelId"
:round="false"
:value="config.desktop.minimizeToTrayOnClose"
:loading="pendingFields.has('minimizeToTrayOnClose')"
:disabled="pendingFields.has('minimizeToTrayOnClose')"
@update:value="updateSetting('minimizeToTrayOnClose', { desktop: { minimizeToTrayOnClose: $event } })"
/>
</div>
</div>
</div>
</section>
<section class="desktop-general-settings__section">
<h2 class="desktop-general-settings__title">
{{ t('desktop.settings.contextPanel') }}
Expand Down
Loading