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
45 changes: 43 additions & 2 deletions packages/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* IPC bridge and ACP session hub wired in.
*/

import { app, BrowserWindow, Menu, shell, nativeTheme } from 'electron';
import { app, BrowserWindow, Menu, shell, nativeTheme, Tray, nativeImage } from 'electron';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { registerIpc } from './ipc.js';
Expand All @@ -19,10 +19,13 @@ import type { UpdateManager } from './updater.js';
// taskbar icon at runtime (dev + Linux/Windows). On macOS the dock icon comes
// from the packaged .app bundle, so this is harmless there.
import appIcon from '../../build/icon.png?asset';
import { getUserSettings } from './userSettings.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null;
let isQuitting = false;
let hub: SessionHub | null = null;
let feishu: FeishuManager | null = null;
let updater: UpdateManager | null = null;
Expand Down Expand Up @@ -94,6 +97,18 @@ function createWindow(): void {

mainWindow.on('ready-to-show', () => mainWindow?.show());

// Intercept window close: if minimizeToTray is enabled, hide instead of close
mainWindow.on('close', (e) => {
const settings = getUserSettings();
if (settings.minimizeToTray && !isQuitting) {
e.preventDefault();
mainWindow?.hide();
if (tray && !tray.isDestroyed()) {
// Tray icon click restores window; no IPC needed
}
}
});

// Open target=_blank / external links in the system browser, never in-app.
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
void shell.openExternal(url);
Expand Down Expand Up @@ -133,6 +148,25 @@ app.whenReady().then(() => {
}
({ hub, feishu, updater } = registerIpc(() => mainWindow));
createWindow();

// Create system tray
const trayIcon = nativeImage.createFromPath(appIcon);
tray = new Tray(trayIcon.resize({ width: 16, height: 16 }));
tray.setToolTip('Easy Code');
tray.on('click', () => {
if (mainWindow) {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
mainWindow.focus();
}
}
});
tray.setContextMenu(Menu.buildFromTemplate([
{ label: 'Show Easy Code', click: () => { mainWindow?.show(); mainWindow?.focus(); } },
{ label: 'Quit', click: () => { isQuitting = true; app.quit(); } },
]));
// Kick off the version-update lifecycle (startup check + periodic poll). It
// delays its first check internally so it never competes with boot.
updater.start();
Expand All @@ -142,11 +176,18 @@ app.whenReady().then(() => {
});
});

app.on('window-all-closed', () => {
app.on('window-all-closed', (e: Event) => {
// In tray mode, prevent app from quitting when all windows are closed
const settings = getUserSettings();
if (settings.minimizeToTray && !isQuitting) {
e.preventDefault();
return;
}
if (process.platform !== 'darwin') app.quit();
});

app.on('before-quit', () => {
isQuitting = true;
hub?.disposeAll();
// Tear down the desktop-managed Feishu gateway so we never leave an orphan
// gateway behind (which the next launch would otherwise detect + kill).
Expand Down
4 changes: 4 additions & 0 deletions packages/desktop/src/main/userSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ function project(raw: Record<string, unknown>): DesktopUserSettings {
) {
out.projectMemoryMode = raw.projectMemoryMode;
}
if (typeof raw.minimizeToTray === 'boolean') out.minimizeToTray = raw.minimizeToTray;
return out;
}

Expand Down Expand Up @@ -90,6 +91,9 @@ export function updateUserSettings(patch: DesktopUserSettings): DesktopUserSetti
if ('projectMemoryMode' in patch && patch.projectMemoryMode) {
raw.projectMemoryMode = patch.projectMemoryMode;
}
if ('minimizeToTray' in patch && typeof patch.minimizeToTray === 'boolean') {
raw.minimizeToTray = patch.minimizeToTray;
}

writeRaw(raw);
return project(raw);
Expand Down
13 changes: 13 additions & 0 deletions packages/desktop/src/renderer/src/components/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,19 @@ function GeneralTab({ onClose }: { onClose: () => void }) {
<div className="setting-desc">{t('settings.healthyUseDesc')}</div>
</div>

<div className="setting-item">
<label className="field-label">Background</label>
<label className="toggle-row">
<input
type="checkbox"
checked={settings?.minimizeToTray === true}
onChange={(e) => void patch({ minimizeToTray: e.target.checked })}
/>
Keep running in background (minimize to tray on close)
</label>
<div className="setting-desc">When enabled, closing the window minimizes Easy Code to the system tray. AI tasks continue running in the background. Click the tray icon to restore.</div>
</div>

<div className="setting-item">
<label className="field-label">{t('update.section')}</label>
<div className="update-check-row">
Expand Down
2 changes: 2 additions & 0 deletions packages/desktop/src/shared/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,8 @@ export interface DesktopUserSettings {
healthyUse?: boolean;
/** How project memory (DEEPV.md / AGENTS.md) is loaded. Undefined = "all". */
projectMemoryMode?: ProjectMemoryMode;
/** Minimize to system tray instead of quitting when window is closed. */
minimizeToTray?: boolean;
}

export interface CreateSessionOptions {
Expand Down
6 changes: 6 additions & 0 deletions packages/vscode-ui-plugin/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2574,6 +2574,8 @@ function setupLoginHandlers() {
});
} else if (config && config.setModel) {
config.setModel(payload.modelName);
// Fix: notify frontend when model is set via config.setModel (no geminiClient)
await communicationService.sendModelSwitchComplete(payload.sessionId, payload.modelName);
}
}

Expand All @@ -2599,6 +2601,10 @@ function setupLoginHandlers() {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
});
// Fix: notify frontend to clear isModelSwitching state on failure
if (payload.sessionId) {
await communicationService.sendModelSwitchComplete(payload.sessionId, payload.modelName);
}
}
});

Expand Down