Skip to content

Commit 95ffbb1

Browse files
Merge branch 'main' into fix/chat-message-direction
2 parents da9bcf6 + 4a4c6dd commit 95ffbb1

333 files changed

Lines changed: 21114 additions & 5028 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ dist-electron/
3030
.electron-runtime/
3131
.showcase/
3232
apps/mobile/.showcase/
33+
apps/mobile/.generated/
34+
/.generated/
3335
artifacts/app-store/screenshots/
3436
.github/pr-assets/
3537
native/**/target/

‎apps/desktop/src/electron/ElectronShell.test.ts‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,16 @@ describe("ElectronShell", () => {
9393
openExternalMock.mockResolvedValue(undefined);
9494

9595
const electronShell = yield* ElectronShell.ElectronShell;
96-
const result = yield* electronShell.openExternal("zed://ssh/example.com/home/user/project");
96+
const results = yield* Effect.all([
97+
electronShell.openExternal("zed://ssh/example.com/home/user/project"),
98+
electronShell.openExternal("zed://ssh/example.com/"),
99+
]);
97100

98-
assert.equal(result, true);
99-
assert.deepEqual(openExternalMock.mock.calls, [["zed://ssh/example.com/home/user/project"]]);
101+
assert.deepEqual(results, [true, true]);
102+
assert.deepEqual(openExternalMock.mock.calls, [
103+
["zed://ssh/example.com/home/user/project"],
104+
["zed://ssh/example.com/"],
105+
]);
100106
}).pipe(Effect.provide(ElectronShell.layer)),
101107
);
102108

‎apps/desktop/src/electron/ElectronShell.ts‎

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { MAC_PERMISSION_SETTINGS_URLS } from "../permissions/MacPermission.ts";
12
import {
23
REMOTE_CAPABLE_EDITOR_IDS,
34
remoteSchemeForEditor,
@@ -10,20 +11,6 @@ import * as Option from "effect/Option";
1011

1112
import * as Electron from "electron";
1213

13-
/**
14-
* Deep links to individual System Settings panes. These are app-fixed, not
15-
* renderer-supplied, so they skip `parseSafeExternalUrl` — which exists to keep
16-
* arbitrary link schemes from reaching the OS handler — and open through their
17-
* own path below. The pane rather than the URL crosses the IPC boundary, so a
18-
* renderer can only ask for one of these known destinations.
19-
*
20-
* Full Disk Access uses the post-Ventura `PrivacySecurity.extension` anchor.
21-
*/
22-
const SYSTEM_SETTINGS_URLS: Record<SystemSettingsPane, string> = {
23-
"full-disk-access":
24-
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AllFiles",
25-
};
26-
2714
// Remote open-in-editor deep links (`vscode://vscode-remote/ssh-remote+…`,
2815
// `zed://ssh/<host>/<path>`) must reach the OS handler; every other non-web
2916
// scheme stays blocked.
@@ -36,7 +23,7 @@ const REMOTE_EDITOR_PROTOCOLS = new Set(
3623
);
3724

3825
// Zed's host sits in the first path segment, so it needs its own userinfo ban.
39-
const ZED_SSH_PATHNAME = /^\/[^/@:]+\/.+$/;
26+
const ZED_SSH_PATHNAME = /^\/[^/@:]+\/.*$/;
4027

4128
const isRemoteEditorUrl = (url: URL) =>
4229
REMOTE_EDITOR_PROTOCOLS.has(url.protocol) &&
@@ -88,7 +75,7 @@ export const make = ElectronShell.of({
8875
}),
8976
openSystemSettings: (pane) =>
9077
Effect.promise(() =>
91-
Electron.shell.openExternal(SYSTEM_SETTINGS_URLS[pane]).then(
78+
Electron.shell.openExternal(MAC_PERMISSION_SETTINGS_URLS[pane]).then(
9279
() => true,
9380
() => false,
9481
),

‎apps/desktop/src/ipc/DesktopIpcHandlers.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
getWindowFullscreenState,
4040
openExternal,
4141
openSystemSettings,
42+
checkSystemPermission,
4243
probeRemoteEditors,
4344
pickFolder,
4445
pickProjectFavicon,
@@ -122,6 +123,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
122123
yield* ipc.handle(showContextMenu);
123124
yield* ipc.handle(openExternal);
124125
yield* ipc.handle(openSystemSettings);
126+
yield* ipc.handle(checkSystemPermission);
125127
yield* ipc.handle(probeRemoteEditors);
126128
yield* ipc.handle(getUpdateState);
127129
yield* ipc.handle(setUpdateChannel);

‎apps/desktop/src/ipc/channels.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,7 @@ export const PREVIEW_RECORDING_SAVE_CHANNEL = "desktop:preview-recording-save";
105105
export const PREVIEW_RECORDING_FRAME_CHANNEL = "desktop:preview-recording-frame";
106106
export const PREVIEW_STATE_CHANGE_CHANNEL = "desktop:preview-state-change";
107107
export const PREVIEW_POINTER_EVENT_CHANNEL = "desktop:preview-pointer-event";
108+
109+
export const MAC_PERMISSION_HELPER_CHANNEL = "desktop:mac-permission-helper";
110+
111+
export const CHECK_SYSTEM_PERMISSION_CHANNEL = "desktop:check-system-permission";

‎apps/desktop/src/ipc/methods/window.ts‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ import * as ElectronMenu from "../../electron/ElectronMenu.ts";
3434
import * as ElectronShell from "../../electron/ElectronShell.ts";
3535
import * as ElectronTheme from "../../electron/ElectronTheme.ts";
3636
import * as ElectronWindow from "../../electron/ElectronWindow.ts";
37+
import * as Electron from "electron";
38+
import * as MacPermissions from "../../permissions/MacPermissions.ts";
39+
import { safariPermissionCheck } from "../../preview/BrowserImport/SafariPermission.ts";
3740
import * as IpcChannels from "../channels.ts";
3841
import * as DesktopIpc from "../DesktopIpc.ts";
3942
import {
@@ -305,7 +308,16 @@ export const openSystemSettings = DesktopIpc.makeIpcMethod({
305308
result: Schema.Boolean,
306309
handler: Effect.fn("desktop.ipc.window.openSystemSettings")(function* (pane) {
307310
const shell = yield* ElectronShell.ElectronShell;
308-
return yield* shell.openSystemSettings(pane);
311+
const environment = yield* DesktopEnvironment.DesktopEnvironment;
312+
if (environment.platform !== "darwin") return false;
313+
const owner = Electron.BrowserWindow.getFocusedWindow();
314+
const opened = yield* shell.openSystemSettings(pane);
315+
if (opened && environment.isPackaged) {
316+
const permissions = yield* MacPermissions.MacPermissions;
317+
const isGranted = yield* safariPermissionCheck;
318+
yield* permissions.showHelper(pane, owner, isGranted);
319+
}
320+
return opened;
309321
}),
310322
});
311323

@@ -379,3 +391,15 @@ export const pickThemeFiles = DesktopIpc.makeIpcMethod({
379391
});
380392
}),
381393
});
394+
395+
export const checkSystemPermission = DesktopIpc.makeIpcMethod({
396+
channel: IpcChannels.CHECK_SYSTEM_PERMISSION_CHANNEL,
397+
payload: SystemSettingsPaneSchema,
398+
result: Schema.Boolean,
399+
handler: Effect.fn("desktop.ipc.window.checkSystemPermission")(function* () {
400+
const environment = yield* DesktopEnvironment.DesktopEnvironment;
401+
if (environment.platform !== "darwin") return false;
402+
const check = yield* safariPermissionCheck;
403+
return yield* Effect.promise(check);
404+
}),
405+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { ipcRenderer } from "electron";
2+
import { MAC_PERMISSION_HELPER_CHANNEL } from "./ipc/channels.ts";
3+
4+
// This preload belongs only to the static permission panel. No general desktop bridge is exposed.
5+
window.addEventListener("DOMContentLoaded", () => {
6+
const send = (action: "drag" | "finder" | "close") =>
7+
ipcRenderer.send(MAC_PERMISSION_HELPER_CHANNEL, action);
8+
document.getElementById("app")?.addEventListener("dragstart", (event) => {
9+
event.preventDefault();
10+
send("drag");
11+
});
12+
document.getElementById("app")?.addEventListener("click", () => send("finder"));
13+
document.getElementById("close")?.addEventListener("click", () => send("close"));
14+
document.addEventListener("keydown", (event) => {
15+
if (event.key === "Escape") send("close");
16+
});
17+
});

‎apps/desktop/src/main.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as MacPermissions from "./permissions/MacPermissions.ts";
12
for (const stream of [process.stdout, process.stderr]) {
23
stream.on("error", (err: NodeJS.ErrnoException) => {
34
if (err.code !== "EPIPE") throw err;
@@ -133,6 +134,7 @@ const electronLayer = Layer.mergeAll(
133134
);
134135

135136
const desktopFoundationLayer = Layer.mergeAll(
137+
MacPermissions.layer,
136138
DesktopState.layer,
137139
DesktopShutdown.layer,
138140
DesktopAppSettings.layer,
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const MAC_PERMISSION_SETTINGS_URLS = {
2+
"screen-recording":
3+
"x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture",
4+
accessibility: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility",
5+
"full-disk-access":
6+
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AllFiles",
7+
};
8+
9+
export type MacPermission = keyof typeof MAC_PERMISSION_SETTINGS_URLS;
10+
11+
export const MAC_PERMISSION_TITLES: Record<MacPermission, string> = {
12+
"screen-recording": "Screen Recording",
13+
accessibility: "Accessibility",
14+
"full-disk-access": "Full Disk Access",
15+
};

0 commit comments

Comments
 (0)