Skip to content
Merged
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
83 changes: 51 additions & 32 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,19 @@ import type { MenuItemConstructorOptions } from "electron";
import * as Effect from "effect/Effect";
import type {
DesktopPreviewBounds,
DesktopPreviewState,
DesktopTheme,
DesktopUpdateActionResult,
DesktopUpdateState,
PreviewTabId,
PreviewTabsState,
} from "@okcode/contracts";
import { autoUpdater } from "electron-updater";

import type { ContextMenuItem } from "@okcode/contracts";
import { NetService } from "@okcode/shared/Net";
import { RotatingFileSink } from "@okcode/shared/logging";
import { showDesktopConfirmDialog } from "./confirmDialog";
import { createClosedPreviewState } from "./preview";
import { createEmptyTabsState } from "./preview";
import { DesktopPreviewController } from "./previewController";
import { syncShellEnvironment } from "./syncShellEnvironment";
import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState";
Expand Down Expand Up @@ -62,15 +63,18 @@ const UPDATE_STATE_CHANNEL = "desktop:update-state";
const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";
const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download";
const UPDATE_INSTALL_CHANNEL = "desktop:update-install";
const PREVIEW_OPEN_CHANNEL = "desktop:preview-open";
const PREVIEW_CLOSE_CHANNEL = "desktop:preview-close";
const PREVIEW_CREATE_TAB_CHANNEL = "desktop:preview-create-tab";
const PREVIEW_CLOSE_TAB_CHANNEL = "desktop:preview-close-tab";
const PREVIEW_ACTIVATE_TAB_CHANNEL = "desktop:preview-activate-tab";
const PREVIEW_GO_BACK_CHANNEL = "desktop:preview-go-back";
const PREVIEW_GO_FORWARD_CHANNEL = "desktop:preview-go-forward";
const PREVIEW_RELOAD_CHANNEL = "desktop:preview-reload";
const PREVIEW_NAVIGATE_CHANNEL = "desktop:preview-navigate";
const PREVIEW_TOGGLE_DEVTOOLS_CHANNEL = "desktop:preview-toggle-devtools";
const PREVIEW_GET_STATE_CHANNEL = "desktop:preview-get-state";
const PREVIEW_SET_BOUNDS_CHANNEL = "desktop:preview-set-bounds";
const PREVIEW_STATE_CHANNEL = "desktop:preview-state";
const PREVIEW_CLOSE_ALL_CHANNEL = "desktop:preview-close-all";
const PREVIEW_TABS_STATE_CHANNEL = "desktop:preview-tabs-state";
const BASE_DIR = process.env.OKCODE_HOME?.trim() || Path.join(OS.homedir(), ".okcode");
const STATE_DIR = Path.join(BASE_DIR, "userdata");
const DESKTOP_SCHEME = "okcode";
Expand Down Expand Up @@ -749,11 +753,11 @@ function emitUpdateState(): void {
}
}

function emitPreviewState(window: BrowserWindow, state: DesktopPreviewState): void {
function emitPreviewState(window: BrowserWindow, state: PreviewTabsState): void {
if (window.isDestroyed()) {
return;
}
window.webContents.send(PREVIEW_STATE_CHANNEL, state);
window.webContents.send(PREVIEW_TABS_STATE_CHANNEL, state);
}

function getPreviewController(window: BrowserWindow): DesktopPreviewController {
Expand Down Expand Up @@ -1279,27 +1283,33 @@ function registerIpcHandlers(): void {
} satisfies DesktopUpdateActionResult;
});

ipcMain.removeHandler(PREVIEW_OPEN_CHANNEL);
ipcMain.handle(PREVIEW_OPEN_CHANNEL, async (event, input: { url?: unknown; title?: unknown }) => {
ipcMain.removeHandler(PREVIEW_CREATE_TAB_CHANNEL);
ipcMain.handle(
PREVIEW_CREATE_TAB_CHANNEL,
async (event, input: { url?: unknown; title?: unknown }) => {
const window = resolvePreviewWindow(event.sender);
if (!window) {
return { tabId: "", state: createEmptyTabsState() };
}
return getPreviewController(window).createTab({
url: input?.url,
title: input?.title,
});
},
);

ipcMain.removeHandler(PREVIEW_CLOSE_TAB_CHANNEL);
ipcMain.handle(PREVIEW_CLOSE_TAB_CHANNEL, async (event, input: { tabId?: PreviewTabId }) => {
const window = resolvePreviewWindow(event.sender);
if (!window) {
return {
accepted: false,
state: getPreviewController(mainWindow ?? createWindow()).getState(),
};
}
const controller = getPreviewController(window);
return controller.open({
url: input?.url,
title: input?.title,
});
if (!window || !input?.tabId) return createEmptyTabsState();
return getPreviewController(window).closeTab(input.tabId);
});

ipcMain.removeHandler(PREVIEW_CLOSE_CHANNEL);
ipcMain.handle(PREVIEW_CLOSE_CHANNEL, async (event) => {
ipcMain.removeHandler(PREVIEW_ACTIVATE_TAB_CHANNEL);
ipcMain.handle(PREVIEW_ACTIVATE_TAB_CHANNEL, async (event, input: { tabId?: PreviewTabId }) => {
const window = resolvePreviewWindow(event.sender);
if (!window) return;
getPreviewController(window).close();
if (!window || !input?.tabId) return createEmptyTabsState();
return getPreviewController(window).activateTab(input.tabId);
});

ipcMain.removeHandler(PREVIEW_GO_BACK_CHANNEL);
Expand Down Expand Up @@ -1327,21 +1337,23 @@ function registerIpcHandlers(): void {
ipcMain.handle(PREVIEW_NAVIGATE_CHANNEL, async (event, input: { url?: unknown }) => {
const window = resolvePreviewWindow(event.sender);
if (!window) {
return {
accepted: false,
state: getPreviewController(mainWindow ?? createWindow()).getState(),
};
return { accepted: false, state: createEmptyTabsState() };
}
return getPreviewController(window).navigate({
url: input?.url,
});
return getPreviewController(window).navigate({ url: input?.url });
});

ipcMain.removeHandler(PREVIEW_TOGGLE_DEVTOOLS_CHANNEL);
ipcMain.handle(PREVIEW_TOGGLE_DEVTOOLS_CHANNEL, async (event) => {
const window = resolvePreviewWindow(event.sender);
if (!window) return;
getPreviewController(window).toggleDevTools();
});

ipcMain.removeHandler(PREVIEW_GET_STATE_CHANNEL);
ipcMain.handle(PREVIEW_GET_STATE_CHANNEL, async (event) => {
const window = resolvePreviewWindow(event.sender);
if (!window) {
return createClosedPreviewState();
return createEmptyTabsState();
}
return getPreviewController(window).getState();
});
Expand All @@ -1352,6 +1364,13 @@ function registerIpcHandlers(): void {
if (!window) return;
getPreviewController(window).setBounds(bounds);
});

ipcMain.removeHandler(PREVIEW_CLOSE_ALL_CHANNEL);
ipcMain.handle(PREVIEW_CLOSE_ALL_CHANNEL, async (event) => {
const window = resolvePreviewWindow(event.sender);
if (!window) return;
getPreviewController(window).closeAll();
});
}

function getIconOption(): { icon: string } | Record<string, never> {
Expand Down
22 changes: 14 additions & 8 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@ const UPDATE_STATE_CHANNEL = "desktop:update-state";
const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";
const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download";
const UPDATE_INSTALL_CHANNEL = "desktop:update-install";
const PREVIEW_OPEN_CHANNEL = "desktop:preview-open";
const PREVIEW_CLOSE_CHANNEL = "desktop:preview-close";
const PREVIEW_CREATE_TAB_CHANNEL = "desktop:preview-create-tab";
const PREVIEW_CLOSE_TAB_CHANNEL = "desktop:preview-close-tab";
const PREVIEW_ACTIVATE_TAB_CHANNEL = "desktop:preview-activate-tab";
const PREVIEW_GO_BACK_CHANNEL = "desktop:preview-go-back";
const PREVIEW_GO_FORWARD_CHANNEL = "desktop:preview-go-forward";
const PREVIEW_RELOAD_CHANNEL = "desktop:preview-reload";
const PREVIEW_NAVIGATE_CHANNEL = "desktop:preview-navigate";
const PREVIEW_TOGGLE_DEVTOOLS_CHANNEL = "desktop:preview-toggle-devtools";
const PREVIEW_GET_STATE_CHANNEL = "desktop:preview-get-state";
const PREVIEW_SET_BOUNDS_CHANNEL = "desktop:preview-set-bounds";
const PREVIEW_STATE_CHANNEL = "desktop:preview-state";
const PREVIEW_CLOSE_ALL_CHANNEL = "desktop:preview-close-all";
const PREVIEW_TABS_STATE_CHANNEL = "desktop:preview-tabs-state";
const wsUrl = process.env.OKCODE_DESKTOP_WS_URL ?? null;

contextBridge.exposeInMainWorld("desktopBridge", {
Expand Down Expand Up @@ -59,23 +62,26 @@ contextBridge.exposeInMainWorld("desktopBridge", {
};
},
preview: {
open: (input) => ipcRenderer.invoke(PREVIEW_OPEN_CHANNEL, input),
close: () => ipcRenderer.invoke(PREVIEW_CLOSE_CHANNEL),
createTab: (input) => ipcRenderer.invoke(PREVIEW_CREATE_TAB_CHANNEL, input),
closeTab: (input) => ipcRenderer.invoke(PREVIEW_CLOSE_TAB_CHANNEL, input),
activateTab: (input) => ipcRenderer.invoke(PREVIEW_ACTIVATE_TAB_CHANNEL, input),
goBack: () => ipcRenderer.invoke(PREVIEW_GO_BACK_CHANNEL),
goForward: () => ipcRenderer.invoke(PREVIEW_GO_FORWARD_CHANNEL),
reload: () => ipcRenderer.invoke(PREVIEW_RELOAD_CHANNEL),
navigate: (input) => ipcRenderer.invoke(PREVIEW_NAVIGATE_CHANNEL, input),
getState: () => ipcRenderer.invoke(PREVIEW_GET_STATE_CHANNEL),
toggleDevTools: () => ipcRenderer.invoke(PREVIEW_TOGGLE_DEVTOOLS_CHANNEL),
setBounds: (bounds) => ipcRenderer.invoke(PREVIEW_SET_BOUNDS_CHANNEL, bounds),
closeAll: () => ipcRenderer.invoke(PREVIEW_CLOSE_ALL_CHANNEL),
getState: () => ipcRenderer.invoke(PREVIEW_GET_STATE_CHANNEL),
onState: (listener) => {
const wrappedListener = (_event: Electron.IpcRendererEvent, state: unknown) => {
if (typeof state !== "object" || state === null) return;
listener(state as Parameters<typeof listener>[0]);
};

ipcRenderer.on(PREVIEW_STATE_CHANNEL, wrappedListener);
ipcRenderer.on(PREVIEW_TABS_STATE_CHANNEL, wrappedListener);
return () => {
ipcRenderer.removeListener(PREVIEW_STATE_CHANNEL, wrappedListener);
ipcRenderer.removeListener(PREVIEW_TABS_STATE_CHANNEL, wrappedListener);
};
},
},
Expand Down
32 changes: 5 additions & 27 deletions apps/desktop/src/preview.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { describe, expect, it } from "vitest";

import {
createClosedPreviewState,
createPreviewErrorState,
createEmptyTabsState,
sanitizeDesktopPreviewBounds,
validateDesktopPreviewUrl,
} from "./preview";
Expand Down Expand Up @@ -104,32 +103,11 @@ describe("sanitizeDesktopPreviewBounds", () => {
});

describe("preview state helpers", () => {
it("creates closed and error states with predictable defaults", () => {
expect(createClosedPreviewState()).toEqual({
status: "closed",
url: null,
title: null,
it("creates empty tabs state", () => {
expect(createEmptyTabsState()).toEqual({
tabs: [],
activeTabId: null,
visible: false,
error: null,
canGoBack: false,
canGoForward: false,
});

expect(
createPreviewErrorState("load-failed", "Dev server did not respond.", {
url: "http://localhost:3000/",
}),
).toEqual({
status: "error",
url: "http://localhost:3000/",
title: null,
visible: false,
error: {
code: "load-failed",
message: "Dev server did not respond.",
},
canGoBack: false,
canGoForward: false,
});
});
});
53 changes: 7 additions & 46 deletions apps/desktop/src/preview.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,18 @@
import type {
DesktopPreviewBounds,
DesktopPreviewError,
DesktopPreviewErrorCode,
DesktopPreviewState,
PreviewTabsState,
} from "@okcode/contracts";
import {
sanitizeLocalPreviewBounds,
validateHttpPreviewUrl,
validateLocalPreviewUrl,
} from "@okcode/shared/preview";
import { sanitizeLocalPreviewBounds, validateHttpPreviewUrl } from "@okcode/shared/preview";

const CLOSED_PREVIEW_STATE: DesktopPreviewState = {
status: "closed",
url: null,
title: null,
const EMPTY_TABS_STATE: PreviewTabsState = {
tabs: [],
activeTabId: null,
visible: false,
error: null,
canGoBack: false,
canGoForward: false,
};

function makePreviewError(code: DesktopPreviewErrorCode, message: string): DesktopPreviewError {
return { code, message };
}

export function createClosedPreviewState(): DesktopPreviewState {
return { ...CLOSED_PREVIEW_STATE };
}

export function createPreviewErrorState(
code: DesktopPreviewErrorCode,
message: string,
partial?: Partial<DesktopPreviewState>,
): DesktopPreviewState {
return {
status: "error",
url: partial?.url ?? null,
title: partial?.title ?? null,
visible: false,
error: makePreviewError(code, message),
canGoBack: false,
canGoForward: false,
};
export function createEmptyTabsState(): PreviewTabsState {
return { ...EMPTY_TABS_STATE };
}

export function validateDesktopPreviewUrl(
Expand All @@ -50,16 +21,6 @@ export function validateDesktopPreviewUrl(
return validateHttpPreviewUrl(rawUrl);
}

/**
* Stricter validation that only allows localhost URLs.
* Kept for contexts where only local dev servers should be previewed.
*/
export function validateDesktopLocalPreviewUrl(
rawUrl: unknown,
): { ok: true; url: string } | { ok: false; error: DesktopPreviewError } {
return validateLocalPreviewUrl(rawUrl);
}

export function sanitizeDesktopPreviewBounds(bounds: DesktopPreviewBounds): DesktopPreviewBounds {
return sanitizeLocalPreviewBounds(bounds);
}
Loading
Loading