| 1 | import { |
| 2 | ContextMenuItemSchema, |
| 3 | DesktopAppBrandingSchema, |
| 4 | DesktopEnvironmentBootstrapSchema, |
| 5 | DesktopThemeSchema, |
| 6 | EDITORS, |
| 7 | EditorId, |
| 8 | PickedThemeFileSchema, |
| 9 | PickFolderOptionsSchema, |
| 10 | PRIMARY_LOCAL_ENVIRONMENT_ID, |
| 11 | REMOTE_CAPABLE_EDITOR_IDS, |
| 12 | SystemSettingsPaneSchema, |
| 13 | type DesktopEnvironmentBootstrap, |
| 14 | type PickedThemeFile, |
| 15 | } from "@t3tools/contracts"; |
| 16 | import { PROJECT_FAVICON_EXTENSIONS } from "@t3tools/shared/projectFavicon"; |
| 17 | import { resolveEditorCommand } from "@t3tools/shared/editor"; |
| 18 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 19 | import * as FileSystem from "effect/FileSystem"; |
| 20 | import * as Path from "effect/Path"; |
| 21 | import * as Effect from "effect/Effect"; |
| 22 | import * as Option from "effect/Option"; |
| 23 | import * as Schema from "effect/Schema"; |
| 24 | |
| 25 | import * as DesktopBackendConfiguration from "../../backend/DesktopBackendConfiguration.ts"; |
| 26 | import * as DesktopBackendPool from "../../backend/DesktopBackendPool.ts"; |
| 27 | import * as DesktopLocalEnvironmentAuth from "../../backend/DesktopLocalEnvironmentAuth.ts"; |
| 28 | import * as DesktopEnvironment from "../../app/DesktopEnvironment.ts"; |
| 29 | import * as DesktopAppSettings from "../../settings/DesktopAppSettings.ts"; |
| 30 | import * as DesktopWslBackend from "../../wsl/DesktopWslBackend.ts"; |
| 31 | import * as DesktopWslEnvironment from "../../wsl/DesktopWslEnvironment.ts"; |
| 32 | import * as ElectronApp from "../../electron/ElectronApp.ts"; |
| 33 | import * as ElectronDialog from "../../electron/ElectronDialog.ts"; |
| 34 | import * as ElectronMenu from "../../electron/ElectronMenu.ts"; |
| 35 | import * as ElectronShell from "../../electron/ElectronShell.ts"; |
| 36 | import * as ElectronTheme from "../../electron/ElectronTheme.ts"; |
| 37 | import * as ElectronWindow from "../../electron/ElectronWindow.ts"; |
| 38 | import * as Electron from "electron"; |
| 39 | import * as MacPermissions from "../../permissions/MacPermissions.ts"; |
| 40 | import { safariPermissionCheck } from "../../preview/BrowserImport/SafariPermission.ts"; |
| 41 | import * as IpcChannels from "../channels.ts"; |
| 42 | import * as DesktopIpc from "../DesktopIpc.ts"; |
| 43 | import { |
| 44 | extractDistroFromUncPath, |
| 45 | resolveWslPickFolderDefaultPath, |
| 46 | wslUncPathToLinuxPath, |
| 47 | } from "../../wsl/wslPathParsing.ts"; |
| 48 | |
| 49 | const ContextMenuPosition = Schema.Struct({ |
| 50 | x: Schema.Number, |
| 51 | y: Schema.Number, |
| 52 | }); |
| 53 | |
| 54 | const ContextMenuInput = Schema.Struct({ |
| 55 | items: Schema.Array(ContextMenuItemSchema), |
| 56 | position: Schema.optionalKey(ContextMenuPosition), |
| 57 | }); |
| 58 | |
| 59 | function toWebSocketBaseUrl(httpBaseUrl: URL): string { |
| 60 | const url = new URL(httpBaseUrl.href); |
| 61 | url.protocol = url.protocol === "https:" ? "wss:" : "ws:"; |
| 62 | return url.href; |
| 63 | } |
| 64 | |
| 65 | export const getAppBranding = DesktopIpc.makeSyncIpcMethod({ |
| 66 | channel: IpcChannels.GET_APP_BRANDING_CHANNEL, |
| 67 | result: Schema.NullOr(DesktopAppBrandingSchema), |
| 68 | handler: Effect.fn("desktop.ipc.window.getAppBranding")(function* () { |
| 69 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
| 70 | return environment.branding; |
| 71 | }), |
| 72 | }); |
| 73 | |
| 74 | export const getSystemLocale = DesktopIpc.makeSyncIpcMethod({ |
| 75 | channel: IpcChannels.GET_SYSTEM_LOCALE_CHANNEL, |
| 76 | result: Schema.String, |
| 77 | handler: Effect.fn("desktop.ipc.window.getSystemLocale")(function* () { |
| 78 | const electronApp = yield* ElectronApp.ElectronApp; |
| 79 | return yield* electronApp.systemLocale; |
| 80 | }), |
| 81 | }); |
| 82 | |
| 83 | export const getWindowFullscreenState = DesktopIpc.makeSyncIpcMethod({ |
| 84 | channel: IpcChannels.GET_WINDOW_FULLSCREEN_STATE_CHANNEL, |
| 85 | result: Schema.Boolean, |
| 86 | handler: Effect.fn("desktop.ipc.window.getWindowFullscreenState")(function* () { |
| 87 | const electronWindow = yield* ElectronWindow.ElectronWindow; |
| 88 | const window = yield* electronWindow.currentMainOrFirst; |
| 89 | return Option.isSome(window) && window.value.isFullScreen(); |
| 90 | }), |
| 91 | }); |
| 92 | |
| 93 | export const getLocalEnvironmentBootstraps = DesktopIpc.makeSyncIpcMethod({ |
| 94 | channel: IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL, |
| 95 | result: Schema.Array(DesktopEnvironmentBootstrapSchema), |
| 96 | handler: Effect.fn("desktop.ipc.window.getLocalEnvironmentBootstraps")(function* () { |
| 97 | const pool = yield* DesktopBackendPool.DesktopBackendPool; |
| 98 | const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration; |
| 99 | const instances = yield* pool.list; |
| 100 | const bootstraps: DesktopEnvironmentBootstrap[] = []; |
| 101 | for (const instance of instances) { |
| 102 | const isPrimary = instance.id === PRIMARY_LOCAL_ENVIRONMENT_ID; |
| 103 | const config = yield* instance.currentConfig; |
| 104 | const snapshot = yield* instance.snapshot; |
| 105 | // A secondary backend (e.g. a parallel WSL backend) that hasn't produced |
| 106 | // a config yet (mid-registration, before its first start cycle) or that |
| 107 | // is retrying a *transient* preflight failure (WSL VM still booting, a |
| 108 | // not-yet-built linux server entry) is not listening on a port. We |
| 109 | // surface it as a *pending* bootstrap (null endpoints, no token) so the |
| 110 | // renderer can show a "Connecting…" indicator while it retries — null |
| 111 | // endpoints keep the renderer from dialing the dead port, avoiding the |
| 112 | // needless /api/auth/bootstrap/bearer error cycles a real endpoint would |
| 113 | // trigger. |
| 114 | if (Option.isNone(config) || Option.isSome(config.value.preflightFailure)) { |
| 115 | // Skip the primary (same-origin, no "connecting" affordance) and skip a |
| 116 | // secondary whose preflight failed *fatally* (no node, wrong version, |
| 117 | // missing build tools): it has stopped retrying, so an indefinite |
| 118 | // "Connecting…" would be misleading — its error is surfaced by the |
| 119 | // WSL-state UI instead. |
| 120 | const fatalPreflight = |
| 121 | Option.isSome(config) && |
| 122 | Option.isSome(config.value.preflightFailure) && |
| 123 | config.value.preflightFailure.value.fatal; |
| 124 | const stoppedPreflight = |
| 125 | Option.isSome(config) && |
| 126 | Option.isSome(config.value.preflightFailure) && |
| 127 | (!snapshot.desiredRunning || !snapshot.restartScheduled); |
| 128 | if (isPrimary || fatalPreflight || stoppedPreflight) continue; |
| 129 | bootstraps.push({ |
| 130 | id: instance.id, |
| 131 | label: yield* instance.label, |
| 132 | runningDistro: null, |
| 133 | httpBaseUrl: null, |
| 134 | wsBaseUrl: null, |
| 135 | }); |
| 136 | continue; |
| 137 | } |
| 138 | const { bootstrap, httpBaseUrl } = config.value; |
| 139 | const runningDistro = config.value.runningDistro ?? null; |
| 140 | bootstraps.push({ |
| 141 | id: instance.id, |
| 142 | label: runningDistro === null ? yield* instance.label : `WSL (${runningDistro})`, |
| 143 | runningDistro, |
| 144 | httpBaseUrl: httpBaseUrl.href, |
| 145 | wsBaseUrl: toWebSocketBaseUrl(httpBaseUrl), |
| 146 | // A backend launched with the desktop secret accepts whichever token |
| 147 | // the secret derives for the current window, so hand out that one |
| 148 | // rather than the token frozen into its launch config. Every backend |
| 149 | // the desktop launches (primary, staged or mounted WSL runtime) is the |
| 150 | // server build bundled with this desktop, so it understands the secret. |
| 151 | ...(bootstrap.desktopBootstrapSecret |
| 152 | ? { bootstrapToken: yield* configuration.currentBootstrapToken } |
| 153 | : bootstrap.desktopBootstrapToken |
| 154 | ? { bootstrapToken: bootstrap.desktopBootstrapToken } |
| 155 | : {}), |
| 156 | }); |
| 157 | } |
| 158 | return bootstraps; |
| 159 | }), |
| 160 | }); |
| 161 | |
| 162 | // Pull the distro selection out of a backend instance id like |
| 163 | // "wsl:ubuntu". Returns null for "wsl:default", which is the sentinel |
| 164 | // for "track the user's WSL default distro" and maps to the |
| 165 | // wslEnv-derived default at picker time. |
| 166 | function extractWslDistroFromEnvironmentId(envId: string): string | null { |
| 167 | if (!envId.startsWith(DesktopWslBackend.WSL_INSTANCE_ID_PREFIX)) { |
| 168 | return null; |
| 169 | } |
| 170 | const suffix = envId.slice(DesktopWslBackend.WSL_INSTANCE_ID_PREFIX.length); |
| 171 | return suffix === "default" || suffix.length === 0 ? null : suffix; |
| 172 | } |
| 173 | |
| 174 | export const getLocalEnvironmentBearerToken = DesktopIpc.makeIpcMethod({ |
| 175 | channel: IpcChannels.GET_LOCAL_ENVIRONMENT_BEARER_TOKEN_CHANNEL, |
| 176 | payload: Schema.Void, |
| 177 | result: Schema.String, |
| 178 | handler: Effect.fn("desktop.ipc.window.getLocalEnvironmentBearerToken")(function* () { |
| 179 | const localAuth = yield* DesktopLocalEnvironmentAuth.DesktopLocalEnvironmentAuth; |
| 180 | return yield* localAuth.getBearerToken; |
| 181 | }), |
| 182 | }); |
| 183 | |
| 184 | export const pickFolder = DesktopIpc.makeIpcMethod({ |
| 185 | channel: IpcChannels.PICK_FOLDER_CHANNEL, |
| 186 | payload: Schema.UndefinedOr(PickFolderOptionsSchema), |
| 187 | result: Schema.NullOr(Schema.String), |
| 188 | handler: Effect.fn("desktop.ipc.window.pickFolder")(function* (options) { |
| 189 | const dialog = yield* ElectronDialog.ElectronDialog; |
| 190 | const electronWindow = yield* ElectronWindow.ElectronWindow; |
| 191 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
| 192 | const appSettings = yield* DesktopAppSettings.DesktopAppSettings; |
| 193 | const wslEnvironment = yield* DesktopWslEnvironment.DesktopWslEnvironment; |
| 194 | const settings = yield* appSettings.get; |
| 195 | // A picked path only means something to a backend on this machine. |
| 196 | if (!settings.localEnvironmentEnabled) { |
| 197 | return null; |
| 198 | } |
| 199 | // Three picker modes: |
| 200 | // - targetEnvironmentId omitted: default to the primary picker. Keeps |
| 201 | // the historical behavior unchanged for users who never enabled the |
| 202 | // WSL backend, and is what unfamiliar callers should get out of the |
| 203 | // box. |
| 204 | // - targetEnvironmentId starts with "wsl:": route to the WSL picker |
| 205 | // using the distro encoded in the id (or the user's selected |
| 206 | // wslDistro when the id is the "wsl:default" sentinel). |
| 207 | // - anything else (incl. PRIMARY_LOCAL_ENVIRONMENT_ID): primary picker. |
| 208 | const targetId = options?.targetEnvironmentId; |
| 209 | const wslDistroFromTarget = |
| 210 | targetId !== undefined && targetId.startsWith(DesktopWslBackend.WSL_INSTANCE_ID_PREFIX) |
| 211 | ? extractWslDistroFromEnvironmentId(targetId) |
| 212 | : null; |
| 213 | const useWsl = |
| 214 | targetId !== undefined && |
| 215 | targetId !== PRIMARY_LOCAL_ENVIRONMENT_ID && |
| 216 | targetId.startsWith(DesktopWslBackend.WSL_INSTANCE_ID_PREFIX); |
| 217 | // Fall back to the persisted wslDistro when the id is the |
| 218 | // "wsl:default" sentinel; the orchestrator uses the same fallback |
| 219 | // for the actual backend. |
| 220 | const wslDistro = useWsl ? (wslDistroFromTarget ?? settings.wslDistro) : null; |
| 221 | const defaultPath = useWsl |
| 222 | ? Option.fromNullishOr( |
| 223 | resolveWslPickFolderDefaultPath( |
| 224 | options, |
| 225 | { distro: wslDistro }, |
| 226 | yield* wslEnvironment.listDistros, |
| 227 | Option.getOrNull(yield* wslEnvironment.getUserHome(wslDistro)), |
| 228 | ), |
| 229 | ) |
| 230 | : environment.resolvePickFolderDefaultPath(options); |
| 231 | const selectedPath = yield* dialog.pickFolder({ |
| 232 | owner: yield* electronWindow.focusedMainOrFirst, |
| 233 | defaultPath, |
| 234 | }); |
| 235 | if (Option.isNone(selectedPath)) { |
| 236 | return null; |
| 237 | } |
| 238 | if (!useWsl) { |
| 239 | return selectedPath.value; |
| 240 | } |
| 241 | |
| 242 | const linuxUncPath = wslUncPathToLinuxPath(selectedPath.value); |
| 243 | if (linuxUncPath !== null) { |
| 244 | return linuxUncPath; |
| 245 | } |
| 246 | |
| 247 | const converted = yield* wslEnvironment.windowsToWslPath( |
| 248 | extractDistroFromUncPath(selectedPath.value) ?? wslDistro, |
| 249 | selectedPath.value, |
| 250 | ); |
| 251 | return Option.getOrElse(converted, () => selectedPath.value); |
| 252 | }), |
| 253 | }); |
| 254 | |
| 255 | export const pickProjectFavicon = DesktopIpc.makeIpcMethod({ |
| 256 | channel: IpcChannels.PICK_PROJECT_FAVICON_CHANNEL, |
| 257 | payload: Schema.UndefinedOr(Schema.String), |
| 258 | result: Schema.NullOr(Schema.String), |
| 259 | handler: Effect.fn("desktop.ipc.window.pickProjectFavicon")(function* (initialPath) { |
| 260 | const dialog = yield* ElectronDialog.ElectronDialog; |
| 261 | const electronWindow = yield* ElectronWindow.ElectronWindow; |
| 262 | const appSettings = yield* DesktopAppSettings.DesktopAppSettings; |
| 263 | if (!(yield* appSettings.get).localEnvironmentEnabled) { |
| 264 | return null; |
| 265 | } |
| 266 | const paths = yield* dialog.pickFiles({ |
| 267 | owner: yield* electronWindow.focusedMainOrFirst, |
| 268 | defaultPath: Option.fromNullishOr(initialPath), |
| 269 | multiple: false, |
| 270 | filters: [ |
| 271 | { |
| 272 | name: "Images", |
| 273 | extensions: PROJECT_FAVICON_EXTENSIONS.map((extension) => extension.slice(1)), |
| 274 | }, |
| 275 | ], |
| 276 | }); |
| 277 | return paths[0] ?? null; |
| 278 | }), |
| 279 | }); |
| 280 | |
| 281 | export const setTheme = DesktopIpc.makeIpcMethod({ |
| 282 | channel: IpcChannels.SET_THEME_CHANNEL, |
| 283 | payload: DesktopThemeSchema, |
| 284 | result: Schema.Void, |
| 285 | handler: Effect.fn("desktop.ipc.window.setTheme")(function* (theme) { |
| 286 | const electronTheme = yield* ElectronTheme.ElectronTheme; |
| 287 | yield* electronTheme.setSource(theme); |
| 288 | }), |
| 289 | }); |
| 290 | |
| 291 | export const showContextMenu = DesktopIpc.makeIpcMethod({ |
| 292 | channel: IpcChannels.CONTEXT_MENU_CHANNEL, |
| 293 | payload: ContextMenuInput, |
| 294 | result: Schema.NullOr(Schema.String), |
| 295 | handler: Effect.fn("desktop.ipc.window.showContextMenu")(function* (input) { |
| 296 | const electronMenu = yield* ElectronMenu.ElectronMenu; |
| 297 | const electronWindow = yield* ElectronWindow.ElectronWindow; |
| 298 | const window = yield* electronWindow.focusedMainOrFirst; |
| 299 | if (Option.isNone(window)) { |
| 300 | return null; |
| 301 | } |
| 302 | |
| 303 | const selectedItemId = yield* electronMenu.showContextMenu({ |
| 304 | window: window.value, |
| 305 | items: input.items, |
| 306 | position: Option.fromNullishOr(input.position), |
| 307 | }); |
| 308 | return Option.getOrNull(selectedItemId); |
| 309 | }), |
| 310 | }); |
| 311 | |
| 312 | export const openExternal = DesktopIpc.makeIpcMethod({ |
| 313 | channel: IpcChannels.OPEN_EXTERNAL_CHANNEL, |
| 314 | payload: Schema.String, |
| 315 | result: Schema.Boolean, |
| 316 | handler: Effect.fn("desktop.ipc.window.openExternal")(function* (url) { |
| 317 | const shell = yield* ElectronShell.ElectronShell; |
| 318 | return yield* shell.openExternal(url); |
| 319 | }), |
| 320 | }); |
| 321 | |
| 322 | export const openSystemSettings = DesktopIpc.makeIpcMethod({ |
| 323 | channel: IpcChannels.OPEN_SYSTEM_SETTINGS_CHANNEL, |
| 324 | payload: SystemSettingsPaneSchema, |
| 325 | result: Schema.Boolean, |
| 326 | handler: Effect.fn("desktop.ipc.window.openSystemSettings")(function* (pane) { |
| 327 | const shell = yield* ElectronShell.ElectronShell; |
| 328 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
| 329 | if (environment.platform !== "darwin") return false; |
| 330 | const owner = Electron.BrowserWindow.getFocusedWindow(); |
| 331 | const opened = yield* shell.openSystemSettings(pane); |
| 332 | if (opened && environment.isPackaged) { |
| 333 | const permissions = yield* MacPermissions.MacPermissions; |
| 334 | const isGranted = yield* safariPermissionCheck; |
| 335 | yield* permissions.showHelper(pane, owner, isGranted); |
| 336 | } |
| 337 | return opened; |
| 338 | }), |
| 339 | }); |
| 340 | |
| 341 | export const probeRemoteEditors = DesktopIpc.makeIpcMethod({ |
| 342 | channel: IpcChannels.PROBE_REMOTE_EDITORS_CHANNEL, |
| 343 | payload: Schema.Undefined, |
| 344 | result: Schema.Array(EditorId), |
| 345 | handler: Effect.fn("desktop.ipc.window.probeRemoteEditors")(function* () { |
| 346 | const available: Array<EditorId> = []; |
| 347 | const env = yield* HostProcess.Environment; |
| 348 | for (const editorId of REMOTE_CAPABLE_EDITOR_IDS) { |
| 349 | const editor = EDITORS.find((editor) => editor.id === editorId); |
| 350 | if (editor && Option.isSome(yield* resolveEditorCommand(editor, env))) { |
| 351 | available.push(editorId); |
| 352 | } |
| 353 | } |
| 354 | return available; |
| 355 | }), |
| 356 | }); |
| 357 | |
| 358 | export const pasteAsText = DesktopIpc.makeIpcMethod({ |
| 359 | channel: IpcChannels.PASTE_AS_TEXT_CHANNEL, |
| 360 | payload: Schema.Undefined, |
| 361 | result: Schema.Void, |
| 362 | handler: Effect.fn("desktop.ipc.window.pasteAsText")(function* (_input, event) { |
| 363 | const electronWindow = yield* ElectronWindow.ElectronWindow; |
| 364 | const window = yield* electronWindow.main; |
| 365 | if ( |
| 366 | event === undefined || |
| 367 | Option.isNone(window) || |
| 368 | window.value.isDestroyed() || |
| 369 | window.value.webContents.id !== event.sender.id |
| 370 | ) { |
| 371 | return; |
| 372 | } |
| 373 | const focused = Electron.webContents.getFocusedWebContents(); |
| 374 | if ( |
| 375 | focused && |
| 376 | !focused.isDestroyed() && |
| 377 | Electron.BrowserWindow.fromWebContents(focused) === window.value |
| 378 | ) { |
| 379 | focused.paste(); |
| 380 | } |
| 381 | }), |
| 382 | }); |
| 383 | |
| 384 | /** Theme files are a few KB; anything larger returns empty text and lets the |
| 385 | * renderer reject it by size without the contents ever crossing the bridge. */ |
| 386 | const PICKED_THEME_FILE_MAX_BYTES = 256 * 1024; |
| 387 | |
| 388 | export const pickThemeFiles = DesktopIpc.makeIpcMethod({ |
| 389 | channel: IpcChannels.PICK_THEME_FILES_CHANNEL, |
| 390 | payload: Schema.Undefined, |
| 391 | result: Schema.NullOr(Schema.Array(PickedThemeFileSchema)), |
| 392 | handler: Effect.fn("desktop.ipc.window.pickThemeFiles")(function* () { |
| 393 | const dialog = yield* ElectronDialog.ElectronDialog; |
| 394 | const electronWindow = yield* ElectronWindow.ElectronWindow; |
| 395 | const fileSystem = yield* FileSystem.FileSystem; |
| 396 | const path = yield* Path.Path; |
| 397 | // The VS Code extensions directory is the same dotfolder on Windows, |
| 398 | // macOS, and Linux; when it is missing the picker opens wherever the |
| 399 | // platform would by default. |
| 400 | const extensionsDir = path.join(yield* HostProcess.HomeDirectory, ".vscode", "extensions"); |
| 401 | const defaultPath = yield* fileSystem |
| 402 | .exists(extensionsDir) |
| 403 | .pipe(Effect.orElseSucceed(() => false)); |
| 404 | const paths = yield* dialog.pickFiles({ |
| 405 | owner: yield* electronWindow.focusedMainOrFirst, |
| 406 | defaultPath: defaultPath ? Option.some(extensionsDir) : Option.none(), |
| 407 | filters: [{ name: "JSON", extensions: ["json"] }], |
| 408 | multiple: true, |
| 409 | }); |
| 410 | if (paths.length === 0) { |
| 411 | return null; |
| 412 | } |
| 413 | return yield* Effect.forEach(paths, (filePath) => { |
| 414 | const name = path.basename(filePath); |
| 415 | return Effect.gen(function* () { |
| 416 | const info = yield* fileSystem.stat(filePath); |
| 417 | const size = Number(info.size); |
| 418 | if (size > PICKED_THEME_FILE_MAX_BYTES) { |
| 419 | return { name, size, text: "" } satisfies PickedThemeFile; |
| 420 | } |
| 421 | const text = yield* fileSystem.readFileString(filePath); |
| 422 | return { name, size, text } satisfies PickedThemeFile; |
| 423 | }).pipe( |
| 424 | // An unreadable file degrades to an entry the renderer reports. |
| 425 | Effect.orElseSucceed((): PickedThemeFile => ({ name, size: 0, text: "" })), |
| 426 | ); |
| 427 | }); |
| 428 | }), |
| 429 | }); |
| 430 | |
| 431 | export const checkSystemPermission = DesktopIpc.makeIpcMethod({ |
| 432 | channel: IpcChannels.CHECK_SYSTEM_PERMISSION_CHANNEL, |
| 433 | payload: SystemSettingsPaneSchema, |
| 434 | result: Schema.Boolean, |
| 435 | handler: Effect.fn("desktop.ipc.window.checkSystemPermission")(function* () { |
| 436 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
| 437 | if (environment.platform !== "darwin") return false; |
| 438 | const check = yield* safariPermissionCheck; |
| 439 | return yield* Effect.promise(check); |
| 440 | }), |
| 441 | }); |