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
7 changes: 2 additions & 5 deletions src/main/ipc/localHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { homedir } from "node:os";
import { clipboard, dialog, nativeImage, shell, type BrowserWindow } from "electron";
import type { BrowserPanelManager } from "../browser";
import { openMicrophoneSettings } from "../browser/permissions";
import { showAndFocusWindow } from "../window/showAndFocusWindow";
import {
dbDeleteProject,
dbDeleteThread,
Expand Down Expand Up @@ -116,11 +117,7 @@ export function createLocalIpcHandlers(
openMicrophoneSettings: () => openMicrophoneSettings(),
focusWindow: () => {
const win = options.getMainWindow();
if (!win) return;
if (win.isMinimized()) {
win.restore();
}
win.focus();
if (win) showAndFocusWindow(win);
},
getHomeScopeLocation: () =>
process.platform === "win32"
Expand Down
204 changes: 131 additions & 73 deletions src/main/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { watch } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { app, BrowserWindow, Menu, nativeTheme } from "electron";
import { app, BrowserWindow, globalShortcut, ipcMain, Menu, nativeTheme } from "electron";
import { resolveThemeMode } from "@/shared/themeMode";
import { closeDatabase, dbGetThreads, initDatabase } from "./db";
import { cleanupOrphanedAttachments, prepareLightcodeDataRoot } from "./lightcodeData";
Expand All @@ -21,12 +21,17 @@ import {
import { SupervisorClient } from "./supervisor/SupervisorClient";
import { createAutoUpdaterController } from "./updates/autoUpdater";
import { createMainWindow } from "./window/createMainWindow";
import {
createQuickComposerWindow,
setQuickComposerWindowExpanded,
} from "./window/createQuickComposerWindow";
import { showAndFocusWindow } from "./window/showAndFocusWindow";
import { createTray, type TrayHandle } from "./tray";
import type { SupervisorEvent } from "@/shared/ipc";
import { type LightcodePaths, resolveLightcodeBaseDir } from "@/shared/lightcodePaths";
import { getAppName } from "@/shared/appName";
import { resolveLightcodeChannel } from "@/shared/channel";
import { IPC_EVENT_CHANNELS } from "@/shared/ipc";
import { IPC_EVENT_CHANNELS, IPC_WINDOW_CHANNELS } from "@/shared/ipc";
import { readSharedSettingsFile } from "./sharedSettingsFile";
import { WindowsJobObjectManager } from "./windowsJobObject";
import { captureMainException, initializeMainSentry } from "./diagnostics/sentry";
Expand Down Expand Up @@ -69,8 +74,10 @@ const posthogEnableDev = process.env.POSTHOG_ENABLE_DEV === "1";

const hasSingleInstanceLock = isDev || app.requestSingleInstanceLock();
const WINDOW_CHROME_HEIGHT = 32;
const QUICK_COMPOSER_SHORTCUT = "CommandOrControl+L";

let mainWindow: BrowserWindow | null = null;
let quickComposerWindow: BrowserWindow | null = null;
let lightcodePaths: LightcodePaths | null = null;
let windowsJobObjectManager: WindowsJobObjectManager | null = null;
let browserPanelManager: BrowserPanelManager | null = null;
Expand Down Expand Up @@ -124,6 +131,87 @@ function handleMainWindowClose(event: Electron.Event): void {
mainWindow.hide();
}

function focusMainWindow(): void {
if (mainWindow) showAndFocusWindow(mainWindow);
}

function quickComposerWindowFor(event: Electron.IpcMainInvokeEvent): BrowserWindow | null {
const window = BrowserWindow.fromWebContents(event.sender);
return window && window === quickComposerWindow && !window.isDestroyed() ? window : null;
}

function sendSupervisorEventToRenderers(event: SupervisorEvent): void {
mainWindow?.webContents.send(IPC_EVENT_CHANNELS.supervisorEvent, event);
if (quickComposerWindow && !quickComposerWindow.isDestroyed()) {
quickComposerWindow.webContents.send(IPC_EVENT_CHANNELS.supervisorEvent, event);
}
}

function openQuickComposerWindow(): void {
if (quickComposerWindow && !quickComposerWindow.isDestroyed()) {
showAndFocusWindow(quickComposerWindow);
return;
}

const window = createQuickComposerWindow({
title: getAppName(channel, isDev),
isDev,
channel,
preloadPath: join(__dirname, "preload.cjs"),
rendererHtmlPath: join(__dirname, "../renderer/index.html"),
appVersion: app.getVersion(),
posthogEnableDev,
posthogEnabled,
posthogHost,
posthogKey,
sentryEnabled,
...(process.env.VITE_DEV_SERVER_URL ? { devServerUrl: process.env.VITE_DEV_SERVER_URL } : {}),
onClosed: () => {
if (quickComposerWindow === window) {
quickComposerWindow = null;
}
},
onRendererProcessGone: (details) => {
captureMainException(new Error(`Quick composer renderer process gone: ${details.reason}`), {
"lightcode.feature_area": "quick-composer",
"lightcode.process": "renderer",
});
},
});
quickComposerWindow = window;
}

function createMainAppWindow(): BrowserWindow {
const window = createMainWindow({
title: getAppName(channel, isDev),
isDev,
channel,
preloadPath: join(__dirname, "preload.cjs"),
rendererHtmlPath: join(__dirname, "../renderer/index.html"),
appVersion: app.getVersion(),
posthogEnableDev,
posthogEnabled,
posthogHost,
posthogKey,
sentryEnabled,
windowChromeHeight: WINDOW_CHROME_HEIGHT,
appearance: resolveAppAppearance(),
...(process.env.VITE_DEV_SERVER_URL ? { devServerUrl: process.env.VITE_DEV_SERVER_URL } : {}),
onClosed: () => {
mainWindow = null;
},
onClose: handleMainWindowClose,
onRendererProcessGone: (details) => {
captureMainException(new Error(`Renderer process gone: ${details.reason}`), {
"lightcode.feature_area": "renderer",
"lightcode.process": "renderer",
});
},
});
browserPanelManager?.bindHost(window);
return window;
}

const workingThreads = new Set<string>();
const sleepInhibitor = createSleepInhibitor();

Expand Down Expand Up @@ -165,16 +253,7 @@ if (!hasSingleInstanceLock) {
app.quit();
} else {
app.on("second-instance", () => {
if (!mainWindow) {
return;
}
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
if (!mainWindow.isVisible()) {
mainWindow.show();
}
mainWindow.focus();
focusMainWindow();
});

app.whenReady().then(async () => {
Expand Down Expand Up @@ -236,7 +315,7 @@ if (!hasSingleInstanceLock) {
},
onEvent: (event) => {
handleSupervisorEventForSleep(event);
mainWindow?.webContents.send(IPC_EVENT_CHANNELS.supervisorEvent, event);
sendSupervisorEventToRenderers(event);
},
onReset: () => {
workingThreads.clear();
Expand Down Expand Up @@ -277,34 +356,36 @@ if (!hasSingleInstanceLock) {
callSupervisor: (name, payload) => supervisorClient.call(name, payload),
});

mainWindow = createMainWindow({
title: getAppName(channel, isDev),
isDev,
channel,
preloadPath: join(__dirname, "preload.cjs"),
rendererHtmlPath: join(__dirname, "../renderer/index.html"),
appVersion: app.getVersion(),
posthogEnableDev,
posthogEnabled,
posthogHost,
posthogKey,
sentryEnabled,
windowChromeHeight: WINDOW_CHROME_HEIGHT,
appearance: resolveAppAppearance(),
...(process.env.VITE_DEV_SERVER_URL ? { devServerUrl: process.env.VITE_DEV_SERVER_URL } : {}),
onClosed: () => {
mainWindow = null;
},
onClose: handleMainWindowClose,
onRendererProcessGone: (details) => {
captureMainException(new Error(`Renderer process gone: ${details.reason}`), {
"lightcode.feature_area": "renderer",
"lightcode.process": "renderer",
});
},
ipcMain.handle(IPC_WINDOW_CHANNELS.quickOverlaySetExpanded, (event, expanded: unknown) => {
const window = quickComposerWindowFor(event);
if (!window) return;
setQuickComposerWindowExpanded(window, expanded === true);
});
ipcMain.handle(IPC_WINDOW_CHANNELS.quickOverlayClose, (event) => {
const window = quickComposerWindowFor(event);
if (!window) return;
window.close();
});
ipcMain.handle(IPC_WINDOW_CHANNELS.quickOverlayThreadChanged, (_event, threadId: unknown) => {
if (typeof threadId !== "string" || threadId.length === 0) return;
mainWindow?.webContents.send(IPC_EVENT_CHANNELS.externalAppStoreChanged, { threadId });
});
ipcMain.handle(
IPC_WINDOW_CHANNELS.quickOverlayOpenThreadInMainWindow,
(event, threadId: unknown) => {
if (typeof threadId !== "string" || threadId.length === 0) return;
focusMainWindow();
mainWindow?.webContents.send(IPC_EVENT_CHANNELS.openThreadInMainWindow, { threadId });
const window = quickComposerWindowFor(event);
if (!window) return;
setQuickComposerWindowExpanded(window, false);
setTimeout(() => {
if (!window.isDestroyed()) window.close();
}, 160);
},
);

browserPanelManager.bindHost(mainWindow);
mainWindow = createMainAppWindow();

tray = createTray({
window: mainWindow,
Expand All @@ -316,6 +397,10 @@ if (!hasSingleInstanceLock) {
},
});

if (!globalShortcut.register(QUICK_COMPOSER_SHORTCUT, openQuickComposerWindow)) {
console.warn(`[lightcode] failed to register ${QUICK_COMPOSER_SHORTCUT} for quick composer`);
}

await jobObjectReady;

const hookDebugOn =
Expand Down Expand Up @@ -358,46 +443,15 @@ if (!hasSingleInstanceLock) {

app.on("activate", () => {
if (mainWindow && !mainWindow.isDestroyed()) {
if (!mainWindow.isVisible()) {
mainWindow.show();
}
mainWindow.focus();
showAndFocusWindow(mainWindow);
return;
}
if (BrowserWindow.getAllWindows().length === 0) {
mainWindow = createMainWindow({
title: getAppName(channel, isDev),
isDev,
channel,
preloadPath: join(__dirname, "preload.cjs"),
rendererHtmlPath: join(__dirname, "../renderer/index.html"),
appVersion: app.getVersion(),
posthogEnableDev,
posthogEnabled,
posthogHost,
posthogKey,
sentryEnabled,
windowChromeHeight: WINDOW_CHROME_HEIGHT,
appearance: resolveAppAppearance(),
...(process.env.VITE_DEV_SERVER_URL
? { devServerUrl: process.env.VITE_DEV_SERVER_URL }
: {}),
onClosed: () => {
mainWindow = null;
},
onClose: handleMainWindowClose,
onRendererProcessGone: (details) => {
captureMainException(new Error(`Renderer process gone: ${details.reason}`), {
"lightcode.feature_area": "renderer",
"lightcode.process": "renderer",
});
},
});
}
mainWindow = createMainAppWindow();
});

app.on("before-quit", () => {
isQuitting = true;
globalShortcut.unregister(QUICK_COMPOSER_SHORTCUT);
supervisorClient.dispose();
windowsJobObjectManager?.dispose();
windowsJobObjectManager = null;
Expand All @@ -408,6 +462,10 @@ if (!hasSingleInstanceLock) {
sleepInhibitor.dispose();
tray?.destroy();
tray = null;
if (quickComposerWindow && !quickComposerWindow.isDestroyed()) {
quickComposerWindow.close();
}
quickComposerWindow = null;
});
});
}
Expand Down
38 changes: 38 additions & 0 deletions src/main/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { type LightcodeChannel, normalizeChannel } from "@/shared/channel";
import {
createInvokeBridge,
IPC_EVENT_CHANNELS,
IPC_WINDOW_CHANNELS,
type BrowserEvent,
type LightcodeBridge,
type LightcodeWindowKind,
type SupervisorEvent,
type UpdateStatus,
} from "@/shared/ipc";
Expand Down Expand Up @@ -44,6 +46,11 @@ function resolveChannel(): LightcodeChannel {
return "stable";
}

function resolveWindowKind(): LightcodeWindowKind {
const value = resolveArgValue("--lc-window-kind=");
return value === "quickOverlay" ? "quickOverlay" : "main";
}

function resolveSentryEnabled(): boolean {
const prefix = "--lc-sentry-enabled=";
for (const arg of process.argv) {
Expand Down Expand Up @@ -73,6 +80,7 @@ function resolveArgBoolean(prefix: string): boolean {
}

const bridge: LightcodeBridge = {
windowKind: resolveWindowKind(),
platform: process.platform,
appVersion: resolveAppVersion(),
arch: process.arch,
Expand Down Expand Up @@ -117,6 +125,36 @@ const bridge: LightcodeBridge = {
ipcRenderer.removeListener(IPC_EVENT_CHANNELS.browserEvent, handler);
};
},
setQuickOverlayExpanded(expanded) {
return ipcRenderer.invoke(IPC_WINDOW_CHANNELS.quickOverlaySetExpanded, expanded);
},
closeQuickOverlay() {
return ipcRenderer.invoke(IPC_WINDOW_CHANNELS.quickOverlayClose);
},
notifyQuickOverlayThreadChanged(threadId) {
return ipcRenderer.invoke(IPC_WINDOW_CHANNELS.quickOverlayThreadChanged, threadId);
},
openQuickOverlayThreadInMainWindow(threadId) {
return ipcRenderer.invoke(IPC_WINDOW_CHANNELS.quickOverlayOpenThreadInMainWindow, threadId);
},
onExternalAppStoreChanged(listener) {
const handler = (_event: Electron.IpcRendererEvent, payload: { threadId?: string }) => {
listener(payload);
};
ipcRenderer.on(IPC_EVENT_CHANNELS.externalAppStoreChanged, handler);
return () => {
ipcRenderer.removeListener(IPC_EVENT_CHANNELS.externalAppStoreChanged, handler);
};
},
onOpenThreadInMainWindow(listener) {
const handler = (_event: Electron.IpcRendererEvent, payload: { threadId: string }) => {
listener(payload);
};
ipcRenderer.on(IPC_EVENT_CHANNELS.openThreadInMainWindow, handler);
return () => {
ipcRenderer.removeListener(IPC_EVENT_CHANNELS.openThreadInMainWindow, handler);
};
},
};

contextBridge.exposeInMainWorld("lightcode", bridge);
Loading