Skip to content

Commit dc5877e

Browse files
committed
Merge main into codex/sidebar-session-details
2 parents 62ef014 + 1d78b33 commit dc5877e

393 files changed

Lines changed: 13962 additions & 3232 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.

‎.github/workflows/release-desktop.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ jobs:
478478
shell: bash
479479
run: |
480480
sudo apt-get update
481-
sudo apt-get install -y libsecret-1-dev pkg-config
481+
sudo apt-get install -y libsecret-1-dev pkg-config libgtk-3-0t64 libnss3 libasound2t64 libgbm1
482482
if ! command -v magick >/dev/null 2>&1 && ! command -v convert >/dev/null 2>&1; then
483483
sudo apt-get install -y imagemagick
484484
fi

‎apps/desktop/src/app/DesktopAppIdentity.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const layerElectronApp = (calls: ElectronAppCalls) =>
4242
metadata: Effect.die("unexpected metadata read"),
4343
name: Effect.succeed("T3 Code"),
4444
systemLocale: Effect.succeed("en-US"),
45+
preferredSystemLanguages: Effect.succeed(["en-US"]),
4546
whenReady: Effect.void,
4647
requestSingleInstanceLock: Effect.succeed(true),
4748
quit: Effect.void,

‎apps/desktop/src/app/DesktopLifecycle.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ function layerElectronApp(
3939
metadata: Effect.die("unexpected metadata read"),
4040
name: Effect.succeed("T3 Code"),
4141
systemLocale: Effect.succeed("en-US"),
42+
preferredSystemLanguages: Effect.succeed(["en-US"]),
4243
whenReady: Effect.void,
4344
requestSingleInstanceLock: Effect.succeed(true),
4445
quit,

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export class ElectronApp extends Context.Service<
4646
readonly name: Effect.Effect<string>;
4747
/** Packaged builds ship only en-US; app.getLocale() and Intl defaults do not reflect the OS locale. */
4848
readonly systemLocale: Effect.Effect<string>;
49+
readonly preferredSystemLanguages: Effect.Effect<ReadonlyArray<string>>;
4950
readonly whenReady: Effect.Effect<void, ElectronAppWhenReadyError>;
5051
readonly requestSingleInstanceLock: Effect.Effect<boolean>;
5152
readonly quit: Effect.Effect<void>;
@@ -155,6 +156,7 @@ export const make = ElectronApp.of({
155156
}
156157
return toSystemLocaleTag(identifier, hourCycle);
157158
}),
159+
preferredSystemLanguages: Effect.sync(() => Electron.app.getPreferredSystemLanguages()),
158160
whenReady: Effect.gen(function* () {
159161
const isPackaged = Electron.app.isPackaged;
160162
yield* Effect.tryPromise({

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,67 @@ describe("ElectronShell", () => {
5454
}).pipe(Effect.provide(ElectronShell.layer)),
5555
);
5656

57+
it.effect("opens local editor file deep links", () =>
58+
Effect.gen(function* () {
59+
openExternalMock.mockResolvedValue(undefined);
60+
61+
const urls = [
62+
"cursor://file/Users/me/repo/file.md:186",
63+
"vscode://file/Users/me/repo/file.md:3:7",
64+
"vscode-insiders://file/Users/me/repo/file.md",
65+
"vscodium://file/C:/Users/me/repo/file.md:3",
66+
];
67+
const electronShell = yield* ElectronShell.ElectronShell;
68+
const results = yield* Effect.forEach(urls, (url) => electronShell.openExternal(url));
69+
70+
assert.deepEqual(results, [true, true, true, true]);
71+
assert.deepEqual(
72+
openExternalMock.mock.calls,
73+
urls.map((url) => [url]),
74+
);
75+
}).pipe(Effect.provide(ElectronShell.layer)),
76+
);
77+
78+
it.effect("normalizes the scheme case of local editor file deep links", () =>
79+
Effect.gen(function* () {
80+
openExternalMock.mockResolvedValue(undefined);
81+
82+
const electronShell = yield* ElectronShell.ElectronShell;
83+
const result = yield* electronShell.openExternal("VSCode://file/Users/me/repo/file.md:3");
84+
85+
assert.equal(result, true);
86+
assert.deepEqual(openExternalMock.mock.calls, [["vscode://file/Users/me/repo/file.md:3"]]);
87+
}).pipe(Effect.provide(ElectronShell.layer)),
88+
);
89+
90+
it.effect("does not open editor URLs that are not absolute local file links", () =>
91+
Effect.gen(function* () {
92+
openExternalMock.mockResolvedValue(undefined);
93+
94+
const electronShell = yield* ElectronShell.ElectronShell;
95+
const results = yield* Effect.forEach(
96+
[
97+
"cursor://user@file/Users/me/repo/file.md",
98+
"cursor://:secret@file/Users/me/repo/file.md",
99+
"cursor://file:8080/Users/me/repo/file.md",
100+
"cursor://anysphere.cursor-deeplink/prompt?text=hi",
101+
"vscode://vscode.git/clone?url=https%3A%2F%2Fexample.com%2Frepo.git",
102+
"cursor://file",
103+
"cursor://file/",
104+
"cursor:file/Users/me/repo/file.md",
105+
"foo://file/Users/me/repo/file.md",
106+
"zed://file/Users/me/repo/file.md",
107+
"jetbrains://file/Users/me/repo/file.md",
108+
"javascript:alert(1)",
109+
],
110+
(url) => electronShell.openExternal(url),
111+
);
112+
113+
assert.deepEqual(results, Array(12).fill(false));
114+
assert.equal(openExternalMock.mock.calls.length, 0);
115+
}).pipe(Effect.provide(ElectronShell.layer)),
116+
);
117+
57118
it.effect("does not open editor URLs that mix up link shapes", () =>
58119
Effect.gen(function* () {
59120
openExternalMock.mockResolvedValue(undefined);

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { MAC_PERMISSION_SETTINGS_URLS } from "../permissions/MacPermission.ts";
22
import {
3+
isLocalEditorFileUrl,
34
REMOTE_CAPABLE_EDITOR_IDS,
45
remoteSchemeForEditor,
56
type SystemSettingsPane,
@@ -11,7 +12,7 @@ import * as Option from "effect/Option";
1112

1213
import * as Electron from "electron";
1314

14-
// Remote editor deep links must reach the OS handler.
15+
// Remote editor deep links and local editor file links must reach the OS handler.
1516
const SAFE_WEB_PROTOCOLS = new Set(["http:", "https:"]);
1617
const REMOTE_EDITOR_PROTOCOLS = new Set(
1718
REMOTE_CAPABLE_EDITOR_IDS.flatMap((id) => {
@@ -44,7 +45,9 @@ export function parseSafeExternalUrl(rawUrl: unknown): Option.Option<string> {
4445

4546
try {
4647
const url = new URL(rawUrl);
47-
return SAFE_WEB_PROTOCOLS.has(url.protocol) || isRemoteEditorUrl(url)
48+
return SAFE_WEB_PROTOCOLS.has(url.protocol) ||
49+
isRemoteEditorUrl(url) ||
50+
isLocalEditorFileUrl(url)
4851
? Option.some(url.href)
4952
: Option.none();
5053
} catch {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { installNotificationBadge } from "./methods/notificationBadge.ts";
66
import { getClientSettings, setClientSettings } from "./methods/clientSettings.ts";
77
import { getConnectionCatalog, setConnectionCatalog } from "./methods/connectionCatalog.ts";
88
import {
9+
getHostEnvironmentId,
910
getLocalEnvironmentEnabled,
1011
setLocalEnvironmentEnabled,
1112
} from "./methods/localEnvironment.ts";
@@ -38,6 +39,7 @@ import {
3839
getLocalEnvironmentBearerToken,
3940
installRelayTelemetryEnabled,
4041
getSystemLocale,
42+
getPreferredSystemLanguages,
4143
getWindowFullscreenState,
4244
openExternal,
4345
readMedia,
@@ -94,10 +96,12 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
9496
yield* ipc.handleSync(getAppBranding);
9597
yield* installRelayTelemetryEnabled();
9698
yield* ipc.handleSync(getSystemLocale);
99+
yield* ipc.handleSync(getPreferredSystemLanguages);
97100
yield* ipc.handleSync(getWindowFullscreenState);
98101
yield* ipc.handleSync(getLocalEnvironmentBootstraps);
99102
yield* ipc.handleSync(getLocalEnvironmentEnabled);
100103
yield* ipc.handle(setLocalEnvironmentEnabled);
104+
yield* ipc.handle(getHostEnvironmentId);
101105
yield* ipc.handle(getLocalEnvironmentBearerToken);
102106

103107
yield* ipc.handle(getClientSettings);

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ export const CLI_COMMAND_UNINSTALL_CHANNEL = "desktop:cli-command-uninstall";
3434
export const GET_APP_BRANDING_CHANNEL = "desktop:get-app-branding";
3535
export const GET_RELAY_TELEMETRY_ENABLED_CHANNEL = "desktop:get-relay-telemetry-enabled";
3636
export const GET_SYSTEM_LOCALE_CHANNEL = "desktop:get-system-locale";
37+
export const GET_PREFERRED_SYSTEM_LANGUAGES_CHANNEL = "desktop:get-preferred-system-languages";
3738
export const GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL = "desktop:get-local-environment-bootstraps";
3839
export const GET_LOCAL_ENVIRONMENT_ENABLED_CHANNEL = "desktop:get-local-environment-enabled";
3940
export const SET_LOCAL_ENVIRONMENT_ENABLED_CHANNEL = "desktop:set-local-environment-enabled";
41+
export const GET_HOST_ENVIRONMENT_ID_CHANNEL = "desktop:get-host-environment-id";
4042
export const GET_LOCAL_ENVIRONMENT_BEARER_TOKEN_CHANNEL =
4143
"desktop:get-local-environment-bearer-token";
4244
export const GET_CLIENT_SETTINGS_CHANNEL = "desktop:get-client-settings";

‎apps/desktop/src/ipc/methods/localEnvironment.test.ts‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { assert, describe, it } from "@effect/vitest";
2+
import * as NodeServices from "@effect/platform-node/NodeServices";
3+
import * as FileSystem from "effect/FileSystem";
4+
import * as Path from "effect/Path";
25
import * as Effect from "effect/Effect";
36
import * as Layer from "effect/Layer";
47

@@ -10,7 +13,11 @@ import * as ElectronApp from "../../electron/ElectronApp.ts";
1013
import * as ElectronTheme from "../../electron/ElectronTheme.ts";
1114
import * as DesktopAppSettings from "../../settings/DesktopAppSettings.ts";
1215
import * as DesktopWindow from "../../window/DesktopWindow.ts";
13-
import { getLocalEnvironmentEnabled, setLocalEnvironmentEnabled } from "./localEnvironment.ts";
16+
import {
17+
getHostEnvironmentId,
18+
getLocalEnvironmentEnabled,
19+
setLocalEnvironmentEnabled,
20+
} from "./localEnvironment.ts";
1421

1522
const layerUnusedLifecycleRuntime = Layer.mergeAll(
1623
DesktopShutdown.layer,
@@ -27,6 +34,26 @@ const layerUnusedLifecycleRuntime = Layer.mergeAll(
2734
);
2835

2936
describe("local environment IPC", () => {
37+
it.effect("reads the shared home identity without creating one", () =>
38+
Effect.gen(function* () {
39+
const fs = yield* FileSystem.FileSystem;
40+
const path = yield* Path.Path;
41+
const stateDir = yield* fs.makeTempDirectoryScoped();
42+
const environment = DesktopEnvironment.DesktopEnvironment.of({
43+
path,
44+
stateDir,
45+
} as DesktopEnvironment.DesktopEnvironment["Service"]);
46+
const read = getHostEnvironmentId
47+
.handler(undefined)
48+
.pipe(Effect.provideService(DesktopEnvironment.DesktopEnvironment, environment));
49+
assert.strictEqual(yield* read, null);
50+
assert.isFalse(yield* fs.exists(path.join(stateDir, "environment-id")));
51+
yield* fs.writeFileString(path.join(stateDir, "environment-id"), " service-id\n");
52+
assert.strictEqual(yield* read, "service-id");
53+
yield* fs.writeFileString(path.join(stateDir, "environment-id"), "\n");
54+
assert.strictEqual(yield* read, null);
55+
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
56+
);
3057
it.effect("relaunches only when the setting changes and keeps other settings", () => {
3158
const relaunchReasons: Array<string> = [];
3259
const layer = Layer.mergeAll(

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as Effect from "effect/Effect";
2+
import * as FileSystem from "effect/FileSystem";
23
import * as Schema from "effect/Schema";
34

5+
import * as DesktopEnvironment from "../../app/DesktopEnvironment.ts";
46
import * as DesktopLifecycle from "../../app/DesktopLifecycle.ts";
57
import * as DesktopAppSettings from "../../settings/DesktopAppSettings.ts";
68
import * as IpcChannels from "../channels.ts";
@@ -28,3 +30,18 @@ export const setLocalEnvironmentEnabled = makeIpcMethod({
2830
}
2931
}),
3032
});
33+
34+
// A background service sharing this app's T3 home records its environment ID in the same state dir.
35+
export const getHostEnvironmentId = makeIpcMethod({
36+
channel: IpcChannels.GET_HOST_ENVIRONMENT_ID_CHANNEL,
37+
payload: Schema.Void,
38+
result: Schema.NullOr(Schema.String),
39+
handler: Effect.fn("desktop.ipc.localEnvironment.getHostEnvironmentId")(function* () {
40+
const environment = yield* DesktopEnvironment.DesktopEnvironment;
41+
const fileSystem = yield* FileSystem.FileSystem;
42+
const contents = yield* fileSystem
43+
.readFileString(environment.path.join(environment.stateDir, "environment-id"))
44+
.pipe(Effect.orElseSucceed(() => ""));
45+
return contents.trim() || null;
46+
}),
47+
});

0 commit comments

Comments
 (0)