Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
38 changes: 26 additions & 12 deletions src/main-process/appWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { useDesktopConfig } from '../store/desktopConfig';

/**
* Creates a single application window that displays the renderer and encapsulates all the logic for sending messages to the renderer.
* Closes the application when the window is closed.
* Hides to system tray when the window is closed, keeping the application running.
*/
export class AppWindow {
private readonly appState: IAppState = useAppState();
Expand Down Expand Up @@ -170,10 +170,18 @@ export class AppWindow {

public show(): void {
this.window.show();
if (process.platform === 'darwin') {
app.dock.show().catch((error) => {
log.error('Error showing dock', error);
});
}
}

public hide(): void {
this.window.hide();
if (process.platform === 'darwin') {
app.dock.hide();
}
}

public isMinimized(): boolean {
Expand Down Expand Up @@ -318,7 +326,18 @@ export class AppWindow {

this.window.on('resize', updateBounds);
this.window.on('move', updateBounds);
this.window.on('close', () => log.info('App window closed.'));
this.window.on('close', (event) => {
if (this.appState.isQuitting) {
// App is actually quitting - allow the window to close
log.info('App window closing - app is quitting');
return;
}

// Just hiding to tray - prevent window close
log.info('App window close requested - hiding to tray instead');
event.preventDefault();
this.hide();
});

this.window.webContents.setWindowOpenHandler(({ url }) => {
if (this.#shouldOpenInPopup(url)) {
Expand All @@ -343,6 +362,11 @@ export class AppWindow {
if (this.isMinimized()) this.restore();
this.focus();
});

// Handle activate event (macOS - clicking dock icon when no windows visible)
app.on('activate', () => {
this.show();
});
}

private setupIpcEvents() {
Expand Down Expand Up @@ -418,12 +442,6 @@ export class AppWindow {
label: 'Show Comfy Window',
click: () => {
this.show();
// Mac Only
if (process.platform === 'darwin') {
app.dock.show().catch((error) => {
log.error('Error showing dock', error);
});
}
},
},
{
Expand All @@ -436,10 +454,6 @@ export class AppWindow {
label: 'Hide',
click: () => {
this.hide();
// Mac Only
if (process.platform === 'darwin') {
app.dock.hide();
}
},
},
]);
Expand Down
9 changes: 0 additions & 9 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ initializeAppState();
const overrides = new DevOverrides();

// Register the quit handlers regardless of single instance lock and before squirrel startup events.
quitWhenAllWindowsAreClosed();
trackAppQuitEvents();
initializeSentry();

Expand Down Expand Up @@ -89,14 +88,6 @@ function initalizeLogging() {
log.info(`Starting app v${app.getVersion()}`);
}

/** Quit when all windows are closed.*/
function quitWhenAllWindowsAreClosed() {
app.on('window-all-closed', () => {
log.info('Quitting ComfyUI because window all closed');
app.quit();
});
}

/** Add telemetry for the app quit event. */
function trackAppQuitEvents() {
app.on('quit', (event, exitCode) => {
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/handlers/pathHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const mockFileSystem = ({ exists = true, writable = true, isDirectory = false, c
isDirectory: () => isDirectory,
} as unknown as fs.Stats);
vi.mocked(fs.readdirSync).mockReturnValue(
Array.from({ length: contentLength }, () => ({ name: 'mock-file' }) as fs.Dirent)
Array.from({ length: contentLength }, () => ({ name: 'mock-file' }) as any)
);
if (writable) {
vi.mocked(fs.accessSync).mockReturnValue();
Expand Down
95 changes: 95 additions & 0 deletions tests/unit/main-process/appWindow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import { AppWindow } from '@/main-process/appWindow';

import { type PartialMock, electronMock } from '../setup';

const mockAppState = {
isQuitting: false,
};

vi.mock('@/main-process/appState', () => ({
useAppState: vi.fn(() => mockAppState),
}));

const additionalMocks: PartialMock<typeof Electron> = {
BrowserWindow: vi.fn() as PartialMock<BrowserWindow>,
nativeTheme: {
Expand Down Expand Up @@ -107,3 +115,90 @@ describe('AppWindow.isOnPage', () => {
expect(appWindow.isOnPage('welcome')).toBe(true);
});
});

describe('AppWindow tray behavior', () => {
let mockWindow: PartialMock<BrowserWindow>;
let windowCloseHandler: (event: Electron.Event) => void;

beforeEach(() => {
vi.clearAllMocks();
mockAppState.isQuitting = false;

mockWindow = {
show: vi.fn(),
hide: vi.fn(),
on: vi.fn((event: string, handler: any) => {
if (event === 'close') windowCloseHandler = handler;
}),
once: vi.fn(),
webContents: { getURL: vi.fn(), setWindowOpenHandler: vi.fn() },
};

vi.mocked(BrowserWindow).mockImplementation(() => mockWindow as BrowserWindow);
});

it('should hide to tray when window closed and app not quitting', () => {
vi.stubGlobal('process', { ...process, platform: 'win32', resourcesPath: '/mock' });
new AppWindow();
const mockEvent = { preventDefault: vi.fn() };

windowCloseHandler(mockEvent as any);

expect(mockEvent.preventDefault).toHaveBeenCalled();
expect(mockWindow.hide).toHaveBeenCalled();
});

it('should allow window close when app is quitting', () => {
vi.stubGlobal('process', { ...process, platform: 'win32', resourcesPath: '/mock' });
mockAppState.isQuitting = true;
new AppWindow();
const mockEvent = { preventDefault: vi.fn() };

windowCloseHandler(mockEvent as any);

expect(mockEvent.preventDefault).not.toHaveBeenCalled();
expect(mockWindow.hide).not.toHaveBeenCalled();
});

describe('macOS dock behavior', () => {
beforeEach(() => {
vi.stubGlobal('process', { ...process, platform: 'darwin', resourcesPath: '/mock' });
electronMock.app.dock = {
show: vi.fn().mockResolvedValue(undefined),
hide: vi.fn(),
bounce: vi.fn(),
cancelBounce: vi.fn(),
downloadFinished: vi.fn(),
getBadge: vi.fn(),
setBadge: vi.fn(),
getMenu: vi.fn(),
setMenu: vi.fn(),
setIcon: vi.fn(),
} as any;
});

it('should hide dock when hiding window on macOS', () => {
const appWindow = new AppWindow();

appWindow.hide();

expect(mockWindow.hide).toHaveBeenCalled();
expect(electronMock.app.dock?.hide).toHaveBeenCalled();
});

it('should show dock when showing window on macOS', () => {
const appWindow = new AppWindow();

appWindow.show();

expect(mockWindow.show).toHaveBeenCalled();
expect(electronMock.app.dock?.show).toHaveBeenCalled();
});

it('should register activate handler for dock clicks on macOS', () => {
new AppWindow();

expect(electronMock.app.on).toHaveBeenCalledWith('activate', expect.any(Function));
});
});
});
12 changes: 12 additions & 0 deletions tests/unit/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ export const electronMock: ElectronMock = {
getVersion: vi.fn(() => '1.0.0'),
on: vi.fn(),
once: vi.fn(),
dock: {
show: vi.fn().mockResolvedValue(undefined),
hide: vi.fn(),
bounce: vi.fn(),
cancelBounce: vi.fn(),
downloadFinished: vi.fn(),
getBadge: vi.fn(),
setBadge: vi.fn(),
getMenu: vi.fn(),
setMenu: vi.fn(),
setIcon: vi.fn(),
} as any,
},
dialog: {
showErrorBox: vi.fn(),
Expand Down