1import type {
2 DesktopBridge,
3 DesktopPreviewOpenLinkEvent,
4 DesktopPreviewPointerEvent,
5 DesktopPreviewRecordingInputEvent,
6 DesktopPreviewRecordingFrame,
7 DesktopPreviewTabState,
8 DesktopSnapShotEvent,
9} from "@t3tools/contracts";
10import { exposeClerkBridge } from "@clerk/electron/preload";
11import { contextBridge, ipcRenderer, webFrame, webUtils } from "electron";
12
13import * as IpcChannels from "./ipc/channels.ts";
14import { mergeLegacyLocalStorage } from "./legacyLocalStorageMerge.ts";
15
16const SNAP_SHOT_EVENT_TYPES = new Set([
17 "requested",
18 "started",
19 "ready",
20 "failed",
21 "shortcut-changed",
22]);
23function isSnapShotEvent(value: unknown): value is DesktopSnapShotEvent {
24 if (typeof value !== "object" || value === null) return false;
25 const { type, id } = value as { type?: unknown; id?: unknown };
26 return (
27 typeof type === "string" &&
28 SNAP_SHOT_EVENT_TYPES.has(type) &&
29 (id === undefined || typeof id === "string")
30 );
31}
32
33exposeClerkBridge({ passkeys: true });
34
35// Runs before any app script reads localStorage. See DesktopLegacyLocalStorage.
36try {
37 const legacyItems: unknown = ipcRenderer.sendSync(IpcChannels.TAKE_LEGACY_LOCAL_STORAGE_CHANNEL);
38 if (typeof legacyItems === "object" && legacyItems !== null) {
39 if (mergeLegacyLocalStorage(window.localStorage, legacyItems as Record<string, string>)) {
40 void ipcRenderer.invoke(IpcChannels.COMPLETE_LEGACY_LOCAL_STORAGE_CHANNEL);
41 }
42 }
43} catch {
44 // Best effort: the app still starts on the V2 profile's own storage.
45}
46
47// oxlint-disable-next-line t3code/no-global-process-runtime -- Electron exposes the client platform in its sandboxed preload process.
48const clientPlatform = process.platform;
49
50if (clientPlatform === "darwin") {
51 // Native window buttons do not scale with Chromium zoom. Keep their reserved
52 // space in native points, including when a zoomed page is reloaded.
53 const syncWindowControlInset = () => {
54 document.documentElement.style.setProperty(
55 "--desktop-window-controls-inset",
56 `${90 / webFrame.getZoomFactor()}px`,
57 );
58 };
59 window.addEventListener("DOMContentLoaded", syncWindowControlInset, { once: true });
60 window.addEventListener("resize", syncWindowControlInset);
61}
62
63function unwrapEnsureSshEnvironmentResult(result: unknown) {
64 if (
65 typeof result === "object" &&
66 result !== null &&
67 "type" in result &&
68 result.type === IpcChannels.SSH_PASSWORD_PROMPT_CANCELLED_RESULT
69 ) {
70 const message =
71 "message" in result && typeof result.message === "string"
72 ? result.message
73 : "SSH authentication cancelled.";
74 throw new Error(message);
75 }
76 return result as Awaited<ReturnType<DesktopBridge["ensureSshEnvironment"]>>;
77}
78
79contextBridge.exposeInMainWorld("desktopBridge", {
80 supportsLocalTerminal: (input) =>
81 ipcRenderer.sendSync(`${IpcChannels.DESKTOP_TERMINAL_CHANNEL}:available`, input) === true,
82 openLocalTerminal: (input, onMessage, onClose) => {
83 const { port1, port2 } = new MessageChannel();
84 let closed = false;
85 const close = () => {
86 if (closed) return;
87 closed = true;
88 port1.postMessage(null);
89 port1.close();
90 onClose();
91 };
92 port1.addEventListener("message", ({ data }) => {
93 if (data?.type === "message" && typeof data.data === "string") onMessage(data.data);
94 else if (data?.type === "close") close();
95 });
96 ipcRenderer.postMessage(IpcChannels.DESKTOP_TERMINAL_CHANNEL, input, [port2]);
97 port1.start();
98 return {
99 send: (data) => {
100 if (!closed) port1.postMessage(data);
101 },
102 close,
103 };
104 },
105 getAppBranding: () => {
106 const result = ipcRenderer.sendSync(IpcChannels.GET_APP_BRANDING_CHANNEL);
107 if (typeof result !== "object" || result === null) {
108 return null;
109 }
110 return result as ReturnType<DesktopBridge["getAppBranding"]>;
111 },
112 getPathForFile: (file: File) => webUtils.getPathForFile(file),
113 getClientPlatform: () => clientPlatform,
114 setNotificationBadge: (badge) =>
115 ipcRenderer.invoke(IpcChannels.SET_NOTIFICATION_BADGE_CHANNEL, badge),
116 onNotificationBadgeClear: (listener) => {
117 const handler = () => listener();
118 ipcRenderer.on(IpcChannels.SET_NOTIFICATION_BADGE_CHANNEL, handler);
119 return () => ipcRenderer.removeListener(IpcChannels.SET_NOTIFICATION_BADGE_CHANNEL, handler);
120 },
121 onTrackpadScrollEnd: (listener) => {
122 const handler = () => listener();
123 ipcRenderer.on(IpcChannels.TRACKPAD_SCROLL_END_CHANNEL, handler);
124 return () => ipcRenderer.removeListener(IpcChannels.TRACKPAD_SCROLL_END_CHANNEL, handler);
125 },
126 getSystemLocale: () => {
127 const result = ipcRenderer.sendSync(IpcChannels.GET_SYSTEM_LOCALE_CHANNEL);
128 return typeof result === "string" ? result : null;
129 },
130 getLocalEnvironmentBootstraps: () => {
131 const result = ipcRenderer.sendSync(IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL);
132 if (!Array.isArray(result)) {
133 return [];
134 }
135 return result as ReturnType<DesktopBridge["getLocalEnvironmentBootstraps"]>;
136 },
137 getLocalEnvironmentBearerToken: () =>
138 ipcRenderer.invoke(IpcChannels.GET_LOCAL_ENVIRONMENT_BEARER_TOKEN_CHANNEL),
139 getLocalEnvironmentEnabled: () =>
140 ipcRenderer.sendSync(IpcChannels.GET_LOCAL_ENVIRONMENT_ENABLED_CHANNEL) !== false,
141 setLocalEnvironmentEnabled: (enabled) =>
142 ipcRenderer.invoke(IpcChannels.SET_LOCAL_ENVIRONMENT_ENABLED_CHANNEL, enabled),
143 getClientSettings: () => ipcRenderer.invoke(IpcChannels.GET_CLIENT_SETTINGS_CHANNEL),
144 setClientSettings: (settings) =>
145 ipcRenderer.invoke(IpcChannels.SET_CLIENT_SETTINGS_CHANNEL, settings),
146 requestSnapShotPermissions: (includeAccessibility) =>
147 ipcRenderer.invoke(IpcChannels.REQUEST_SNAP_SHOT_PERMISSIONS_CHANNEL, includeAccessibility),
148 getSnapShotState: () => ipcRenderer.invoke(IpcChannels.GET_SNAP_SHOT_STATE_CHANNEL),
149 setupSnapShot: (action) => ipcRenderer.invoke(IpcChannels.SETUP_SNAP_SHOT_CHANNEL, action),
150 previewSnapShotConfig: (request) =>
151 ipcRenderer.invoke(IpcChannels.PREVIEW_SNAP_SHOT_CONFIG_CHANNEL, request),
152 applySnapShotConfig: (id) => ipcRenderer.invoke(IpcChannels.APPLY_SNAP_SHOT_CONFIG_CHANNEL, id),
153 checkSnapShotShortcut: (shortcut) =>
154 ipcRenderer.invoke(IpcChannels.CHECK_SNAP_SHOT_SHORTCUT_CHANNEL, shortcut),
155 setSnapShotShortcutSuppressed: (suppressed) =>
156 ipcRenderer.invoke(IpcChannels.SET_SNAP_SHOT_SHORTCUT_SUPPRESSED_CHANNEL, suppressed),
157 listPendingSnapShots: () => ipcRenderer.invoke(IpcChannels.LIST_PENDING_SNAP_SHOTS_CHANNEL),
158 readSnapShot: (id) => ipcRenderer.invoke(IpcChannels.READ_SNAP_SHOT_CHANNEL, id),
159 setSnapShotAnimationDestination: (destination) =>
160 ipcRenderer.invoke(IpcChannels.SET_SNAP_SHOT_ANIMATION_DESTINATION_CHANNEL, destination),
161 dismissSnapShotAnimation: (id) =>
162 ipcRenderer.invoke(IpcChannels.DISMISS_SNAP_SHOT_ANIMATION_CHANNEL, id),
163 acknowledgeSnapShot: (id) => ipcRenderer.invoke(IpcChannels.ACKNOWLEDGE_SNAP_SHOT_CHANNEL, id),
164 beginHostedAuth: (server) => ipcRenderer.invoke(IpcChannels.BEGIN_HOSTED_AUTH_CHANNEL, server),
165 getHostedAuthState: () => ipcRenderer.invoke(IpcChannels.GET_HOSTED_AUTH_STATE_CHANNEL),
166 getHostedAccessToken: (server) =>
167 ipcRenderer.invoke(IpcChannels.GET_HOSTED_ACCESS_TOKEN_CHANNEL, server),
168 cancelHostedAuth: () => ipcRenderer.invoke(IpcChannels.CANCEL_HOSTED_AUTH_CHANNEL),
169 disconnectHostedAuth: () => ipcRenderer.invoke(IpcChannels.DISCONNECT_HOSTED_AUTH_CHANNEL),
170 getConnectionCatalog: () => ipcRenderer.invoke(IpcChannels.GET_CONNECTION_CATALOG_CHANNEL),
171 setConnectionCatalog: (catalog) =>
172 ipcRenderer.invoke(IpcChannels.SET_CONNECTION_CATALOG_CHANNEL, catalog),
173 clearConnectionCatalog: () => ipcRenderer.invoke(IpcChannels.CLEAR_CONNECTION_CATALOG_CHANNEL),
174 discoverSshHosts: () => ipcRenderer.invoke(IpcChannels.DISCOVER_SSH_HOSTS_CHANNEL),
175 resolveSshHost: (alias) => ipcRenderer.invoke(IpcChannels.RESOLVE_SSH_HOST_CHANNEL, alias),
176 ensureSshEnvironment: async (target, options) =>
177 unwrapEnsureSshEnvironmentResult(
178 await ipcRenderer.invoke(IpcChannels.ENSURE_SSH_ENVIRONMENT_CHANNEL, {
179 target,
180 ...(options === undefined ? {} : { options }),
181 }),
182 ),
183 disconnectSshEnvironment: (target) =>
184 ipcRenderer.invoke(IpcChannels.DISCONNECT_SSH_ENVIRONMENT_CHANNEL, target),
185 fetchSshEnvironmentDescriptor: (httpBaseUrl) =>
186 ipcRenderer.invoke(IpcChannels.FETCH_SSH_ENVIRONMENT_DESCRIPTOR_CHANNEL, { httpBaseUrl }),
187 bootstrapSshBearerSession: (httpBaseUrl, credential) =>
188 ipcRenderer.invoke(IpcChannels.BOOTSTRAP_SSH_BEARER_SESSION_CHANNEL, {
189 httpBaseUrl,
190 credential,
191 }),
192 fetchSshSessionState: (httpBaseUrl, bearerToken) =>
193 ipcRenderer.invoke(IpcChannels.FETCH_SSH_SESSION_STATE_CHANNEL, { httpBaseUrl, bearerToken }),
194 issueSshWebSocketTicket: (httpBaseUrl, bearerToken) =>
195 ipcRenderer.invoke(IpcChannels.ISSUE_SSH_WEBSOCKET_TOKEN_CHANNEL, { httpBaseUrl, bearerToken }),
196 onSshPasswordPrompt: (listener) => {
197 const wrappedListener = (_event: Electron.IpcRendererEvent, request: unknown) => {
198 if (typeof request !== "object" || request === null) return;
199 listener(request as Parameters<typeof listener>[0]);
200 };
201
202 ipcRenderer.on(IpcChannels.SSH_PASSWORD_PROMPT_CHANNEL, wrappedListener);
203 return () => {
204 ipcRenderer.removeListener(IpcChannels.SSH_PASSWORD_PROMPT_CHANNEL, wrappedListener);
205 };
206 },
207 resolveSshPasswordPrompt: (requestId, password) =>
208 ipcRenderer.invoke(IpcChannels.RESOLVE_SSH_PASSWORD_PROMPT_CHANNEL, { requestId, password }),
209 getServerExposureState: () => ipcRenderer.invoke(IpcChannels.GET_SERVER_EXPOSURE_STATE_CHANNEL),
210 setServerExposureMode: (mode) =>
211 ipcRenderer.invoke(IpcChannels.SET_SERVER_EXPOSURE_MODE_CHANNEL, mode),
212 setTailscaleServeEnabled: (input) =>
213 ipcRenderer.invoke(IpcChannels.SET_TAILSCALE_SERVE_ENABLED_CHANNEL, input),
214 getAdvertisedEndpoints: () => ipcRenderer.invoke(IpcChannels.GET_ADVERTISED_ENDPOINTS_CHANNEL),
215 getWslState: () => ipcRenderer.invoke(IpcChannels.GET_WSL_STATE_CHANNEL),
216 setWslBackendEnabled: (enabled) =>
217 ipcRenderer.invoke(IpcChannels.SET_WSL_BACKEND_ENABLED_CHANNEL, enabled),
218 setWslDistro: (distro) => ipcRenderer.invoke(IpcChannels.SET_WSL_DISTRO_CHANNEL, distro),
219 setWslOnly: (enabled) => ipcRenderer.invoke(IpcChannels.SET_WSL_ONLY_CHANNEL, enabled),
220 pickFolder: (options) => ipcRenderer.invoke(IpcChannels.PICK_FOLDER_CHANNEL, options),
221 pickProjectFavicon: (initialPath) =>
222 ipcRenderer.invoke(IpcChannels.PICK_PROJECT_FAVICON_CHANNEL, initialPath),
223 pickThemeFiles: () => ipcRenderer.invoke(IpcChannels.PICK_THEME_FILES_CHANNEL, undefined),
224 setTheme: (theme) => ipcRenderer.invoke(IpcChannels.SET_THEME_CHANNEL, theme),
225 showContextMenu: (items, position) =>
226 ipcRenderer.invoke(IpcChannels.CONTEXT_MENU_CHANNEL, {
227 items,
228 ...(position === undefined ? {} : { position }),
229 }),
230 receiveProviderAuthCallback: (url: string) =>
231 ipcRenderer.invoke(IpcChannels.RECEIVE_PROVIDER_AUTH_CALLBACK_CHANNEL, url),
232 cancelProviderAuthCallback: (url: string) =>
233 ipcRenderer.invoke(IpcChannels.CANCEL_PROVIDER_AUTH_CALLBACK_CHANNEL, url),
234 openExternal: (url: string) => ipcRenderer.invoke(IpcChannels.OPEN_EXTERNAL_CHANNEL, url),
235 ...(clientPlatform === "darwin"
236 ? {
237 openVideo: (input: { url: string; name: string }) =>
238 ipcRenderer.invoke(IpcChannels.OPEN_VIDEO_CHANNEL, input),
239 }
240 : {}),
241 checkSystemPermission: (pane: string) =>
242 ipcRenderer.invoke(IpcChannels.CHECK_SYSTEM_PERMISSION_CHANNEL, pane),
243 openSystemSettings: (pane: string) =>
244 ipcRenderer.invoke(IpcChannels.OPEN_SYSTEM_SETTINGS_CHANNEL, pane),
245 probeRemoteEditors: () => ipcRenderer.invoke(IpcChannels.PROBE_REMOTE_EDITORS_CHANNEL, undefined),
246 pasteAsText: () => ipcRenderer.invoke(IpcChannels.PASTE_AS_TEXT_CHANNEL, undefined),
247 onMenuAction: (listener) => {
248 const wrappedListener = (_event: Electron.IpcRendererEvent, action: unknown) => {
249 if (typeof action !== "string") return;
250 listener(action);
251 };
252
253 ipcRenderer.on(IpcChannels.MENU_ACTION_CHANNEL, wrappedListener);
254 return () => {
255 ipcRenderer.removeListener(IpcChannels.MENU_ACTION_CHANNEL, wrappedListener);
256 };
257 },
258 onSnapShotEvent: (listener) => {
259 const wrappedListener = (_event: Electron.IpcRendererEvent, event: unknown) => {
260 if (!isSnapShotEvent(event)) return;
261 listener(event);
262 };
263
264 ipcRenderer.on(IpcChannels.SNAP_SHOT_EVENT_CHANNEL, wrappedListener);
265 return () => {
266 ipcRenderer.removeListener(IpcChannels.SNAP_SHOT_EVENT_CHANNEL, wrappedListener);
267 };
268 },
269 onQuitShortcut: (listener) => {
270 const wrappedListener = (_event: Electron.IpcRendererEvent, hint: unknown) => {
271 if (typeof hint !== "object" || hint === null || !("state" in hint)) return;
272 if (hint.state === "up") {
273 listener({ state: "up" });
274 return;
275 }
276 if (
277 hint.state === "down" &&
278 "mode" in hint &&
279 (hint.mode === "hold" || hint.mode === "double-click")
280 ) {
281 listener({ state: "down", mode: hint.mode });
282 }
283 };
284
285 ipcRenderer.on(IpcChannels.QUIT_SHORTCUT_CHANNEL, wrappedListener);
286 return () => {
287 ipcRenderer.removeListener(IpcChannels.QUIT_SHORTCUT_CHANNEL, wrappedListener);
288 };
289 },
290 getWindowFullscreenState: () =>
291 ipcRenderer.sendSync(IpcChannels.GET_WINDOW_FULLSCREEN_STATE_CHANNEL) === true,
292 onWindowFullscreenStateChange: (listener) => {
293 const wrappedListener = (_event: Electron.IpcRendererEvent, fullscreen: unknown) => {
294 if (typeof fullscreen !== "boolean") return;
295 listener(fullscreen);
296 };
297
298 ipcRenderer.on(IpcChannels.WINDOW_FULLSCREEN_STATE_CHANNEL, wrappedListener);
299 return () => {
300 ipcRenderer.removeListener(IpcChannels.WINDOW_FULLSCREEN_STATE_CHANNEL, wrappedListener);
301 };
302 },
303 getUpdateState: () => ipcRenderer.invoke(IpcChannels.UPDATE_GET_STATE_CHANNEL),
304 setUpdateChannel: (channel) =>
305 ipcRenderer.invoke(IpcChannels.UPDATE_SET_CHANNEL_CHANNEL, channel),
306 checkForUpdate: () => ipcRenderer.invoke(IpcChannels.UPDATE_CHECK_CHANNEL),
307 downloadUpdate: () => ipcRenderer.invoke(IpcChannels.UPDATE_DOWNLOAD_CHANNEL),
308 installUpdate: () => ipcRenderer.invoke(IpcChannels.UPDATE_INSTALL_CHANNEL),
309 cliCommand: {
310 getState: () => ipcRenderer.invoke(IpcChannels.CLI_COMMAND_GET_STATE_CHANNEL),
311 install: () => ipcRenderer.invoke(IpcChannels.CLI_COMMAND_INSTALL_CHANNEL),
312 uninstall: () => ipcRenderer.invoke(IpcChannels.CLI_COMMAND_UNINSTALL_CHANNEL),
313 },
314 onUpdateState: (listener) => {
315 const wrappedListener = (_event: Electron.IpcRendererEvent, state: unknown) => {
316 if (typeof state !== "object" || state === null) return;
317 listener(state as Parameters<typeof listener>[0]);
318 };
319
320 ipcRenderer.on(IpcChannels.UPDATE_STATE_CHANNEL, wrappedListener);
321 return () => {
322 ipcRenderer.removeListener(IpcChannels.UPDATE_STATE_CHANNEL, wrappedListener);
323 };
324 },
325 webLinks: {
326 setReady: (ready) => ipcRenderer.invoke(IpcChannels.WEB_LINK_READY_CHANNEL, ready),
327 onOpen: (listener) => {
328 const wrappedListener = (_event: Electron.IpcRendererEvent, url: unknown) => {
329 if (typeof url === "string") listener(url);
330 };
331 ipcRenderer.on(IpcChannels.WEB_LINK_OPEN_CHANNEL, wrappedListener);
332 return () => {
333 ipcRenderer.removeListener(IpcChannels.WEB_LINK_OPEN_CHANNEL, wrappedListener);
334 };
335 },
336 },
337 appActivation: {
338 setReady: (ready) =>
339 ipcRenderer.invoke(IpcChannels.DESKTOP_APP_ACTIVATION_READY_CHANNEL, ready),
340 complete: (response) =>
341 ipcRenderer.invoke(IpcChannels.DESKTOP_APP_ACTIVATION_COMPLETE_CHANNEL, response),
342 onRequest: (listener) => {
343 const wrappedListener = (_event: Electron.IpcRendererEvent, request: unknown) => {
344 if (typeof request !== "object" || request === null) return;
345 listener(request as Parameters<typeof listener>[0]);
346 };
347 ipcRenderer.on(IpcChannels.DESKTOP_APP_ACTIVATION_REQUEST_CHANNEL, wrappedListener);
348 return () => {
349 ipcRenderer.removeListener(
350 IpcChannels.DESKTOP_APP_ACTIVATION_REQUEST_CHANNEL,
351 wrappedListener,
352 );
353 };
354 },
355 },
356 preview: {
357 setForwardedShortcuts: (shortcuts) =>
358 ipcRenderer.invoke(IpcChannels.PREVIEW_SET_FORWARDED_SHORTCUTS_CHANNEL, shortcuts),
359 createTab: (tabId, defaults) =>
360 ipcRenderer.invoke(IpcChannels.PREVIEW_CREATE_TAB_CHANNEL, {
361 tabId,
362 zoomFactor: defaults?.zoomFactor,
363 serverTab: defaults?.serverTab,
364 colorScheme: defaults?.colorScheme,
365 }),
366 closeTab: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_CLOSE_TAB_CHANNEL, { tabId }),
367 registerWebview: (tabId, webContentsId) =>
368 ipcRenderer.invoke(IpcChannels.PREVIEW_REGISTER_WEBVIEW_CHANNEL, { tabId, webContentsId }),
369 navigate: (tabId, url) =>
370 ipcRenderer.invoke(IpcChannels.PREVIEW_NAVIGATE_CHANNEL, { tabId, url }),
371 goBack: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_GO_BACK_CHANNEL, { tabId }),
372 goForward: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_GO_FORWARD_CHANNEL, { tabId }),
373 refresh: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_REFRESH_CHANNEL, { tabId }),
374 zoomIn: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_ZOOM_IN_CHANNEL, { tabId }),
375 zoomOut: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_ZOOM_OUT_CHANNEL, { tabId }),
376 resetZoom: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_RESET_ZOOM_CHANNEL, { tabId }),
377 setZoomFactor: (tabId, zoomFactor) =>
378 ipcRenderer.invoke(IpcChannels.PREVIEW_SET_ZOOM_FACTOR_CHANNEL, { tabId, zoomFactor }),
379 hardReload: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_HARD_RELOAD_CHANNEL, { tabId }),
380 setColorScheme: (tabId, colorScheme) =>
381 ipcRenderer.invoke(IpcChannels.PREVIEW_SET_COLOR_SCHEME_CHANNEL, { tabId, colorScheme }),
382 setAudioMuted: (tabId, audioMuted) =>
383 ipcRenderer.invoke(IpcChannels.PREVIEW_SET_AUDIO_MUTED_CHANNEL, { tabId, audioMuted }),
384 openDevTools: (tabId) =>
385 ipcRenderer.invoke(IpcChannels.PREVIEW_OPEN_DEVTOOLS_CHANNEL, { tabId }),
386 listBrowserImportSources: () => ipcRenderer.invoke(IpcChannels.PREVIEW_IMPORT_SOURCES_CHANNEL),
387 importBrowserCookies: (input) =>
388 ipcRenderer.invoke(IpcChannels.PREVIEW_IMPORT_COOKIES_CHANNEL, input),
389 clearCookies: (environmentId, profileId) =>
390 ipcRenderer.invoke(IpcChannels.PREVIEW_CLEAR_COOKIES_CHANNEL, { environmentId, profileId }),
391 clearCache: (environmentId, profileId) =>
392 ipcRenderer.invoke(IpcChannels.PREVIEW_CLEAR_CACHE_CHANNEL, { environmentId, profileId }),
393 getPreviewConfig: (environmentId, profileId) =>
394 ipcRenderer.invoke(IpcChannels.PREVIEW_GET_CONFIG_CHANNEL, { environmentId, profileId }),
395 setAnnotationTheme: (theme) =>
396 ipcRenderer.invoke(IpcChannels.PREVIEW_SET_ANNOTATION_THEME_CHANNEL, { theme }),
397 setAnnotationSendEnabled: (tabId, enabled) =>
398 ipcRenderer.invoke(IpcChannels.PREVIEW_SET_ANNOTATION_SEND_ENABLED_CHANNEL, {
399 tabId,
400 enabled,
401 }),
402 pickElement: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_PICK_ELEMENT_CHANNEL, { tabId }),
403 cancelPickElement: (tabId) =>
404 ipcRenderer.invoke(IpcChannels.PREVIEW_CANCEL_PICK_ELEMENT_CHANNEL, { tabId }),
405 captureScreenshot: (tabId) =>
406 ipcRenderer.invoke(IpcChannels.PREVIEW_CAPTURE_SCREENSHOT_CHANNEL, { tabId }),
407 revealArtifact: (path) =>
408 ipcRenderer.invoke(IpcChannels.PREVIEW_REVEAL_ARTIFACT_CHANNEL, { path }),
409 copyArtifactToClipboard: (path) =>
410 ipcRenderer.invoke(IpcChannels.PREVIEW_COPY_ARTIFACT_CHANNEL, { path }),
411 pictureInPicture: {
412 open: (tabId) =>
413 ipcRenderer.invoke(IpcChannels.PREVIEW_PICTURE_IN_PICTURE_OPEN_CHANNEL, { tabId }),
414 close: (tabId) =>
415 ipcRenderer.invoke(IpcChannels.PREVIEW_PICTURE_IN_PICTURE_CLOSE_CHANNEL, { tabId }),
416 },
417 recording: {
418 onInput: (listener) => {
419 const wrappedListener = (_event: Electron.IpcRendererEvent, event: unknown) => {
420 if (typeof event !== "object" || event === null) return;
421 listener(event as DesktopPreviewRecordingInputEvent);
422 };
423 ipcRenderer.on(IpcChannels.PREVIEW_RECORDING_INPUT_CHANNEL, wrappedListener);
424 return () =>
425 ipcRenderer.removeListener(IpcChannels.PREVIEW_RECORDING_INPUT_CHANNEL, wrappedListener);
426 },
427 startScreencast: (tabId) =>
428 ipcRenderer.invoke(IpcChannels.PREVIEW_RECORDING_START_CHANNEL, { tabId }),
429 stopScreencast: (tabId) =>
430 ipcRenderer.invoke(IpcChannels.PREVIEW_RECORDING_STOP_CHANNEL, { tabId }),
431 save: (tabId, mimeType, data) =>
432 ipcRenderer.invoke(IpcChannels.PREVIEW_RECORDING_SAVE_CHANNEL, {
433 tabId,
434 mimeType,
435 data,
436 }),
437 onFrame: (listener) => {
438 const wrappedListener = (_event: Electron.IpcRendererEvent, frame: unknown) => {
439 if (typeof frame !== "object" || frame === null) return;
440 listener(frame as DesktopPreviewRecordingFrame);
441 };
442 ipcRenderer.on(IpcChannels.PREVIEW_RECORDING_FRAME_CHANNEL, wrappedListener);
443 return () =>
444 ipcRenderer.removeListener(IpcChannels.PREVIEW_RECORDING_FRAME_CHANNEL, wrappedListener);
445 },
446 },
447 onStateChange: (listener) => {
448 const wrappedListener = (
449 _event: Electron.IpcRendererEvent,
450 tabId: unknown,
451 state: unknown,
452 ) => {
453 if (typeof tabId !== "string" || typeof state !== "object" || state === null) return;
454 listener(tabId, state as DesktopPreviewTabState);
455 };
456 ipcRenderer.on(IpcChannels.PREVIEW_STATE_CHANGE_CHANNEL, wrappedListener);
457 return () =>
458 ipcRenderer.removeListener(IpcChannels.PREVIEW_STATE_CHANGE_CHANNEL, wrappedListener);
459 },
460 onPointerEvent: (listener) => {
461 const wrappedListener = (_event: Electron.IpcRendererEvent, pointerEvent: unknown) => {
462 if (typeof pointerEvent !== "object" || pointerEvent === null) return;
463 listener(pointerEvent as DesktopPreviewPointerEvent);
464 };
465 ipcRenderer.on(IpcChannels.PREVIEW_POINTER_EVENT_CHANNEL, wrappedListener);
466 return () =>
467 ipcRenderer.removeListener(IpcChannels.PREVIEW_POINTER_EVENT_CHANNEL, wrappedListener);
468 },
469 onOpenLink: (listener) => {
470 const wrappedListener = (_event: Electron.IpcRendererEvent, linkEvent: unknown) => {
471 if (typeof linkEvent !== "object" || linkEvent === null) return;
472 listener(linkEvent as DesktopPreviewOpenLinkEvent);
473 };
474 ipcRenderer.on(IpcChannels.PREVIEW_OPEN_LINK_CHANNEL, wrappedListener);
475 return () =>
476 ipcRenderer.removeListener(IpcChannels.PREVIEW_OPEN_LINK_CHANNEL, wrappedListener);
477 },
478 },
479} satisfies DesktopBridge);