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