authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-08-02 23:53:37+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-02 21:53:37+00:00
log69dfb7f09a473d270a8b127cb1c39836fa1c6bc4
treedb0c72f18160769c0fb6b468c0a740301ea02896
parente60821f0e0d82a5d671ca3b94719c49d333921c8
signature Signed by PGP key B5690EEEBB952194

fix(desktop): claim the t3code:// scheme default on Linux at startup (#5054)


6 files changed, 434 insertions(+), 0 deletions(-)

apps/desktop/src/app/DesktopApp.ts+3
...@@ -17,6 +17,7 @@ import * as DesktopWindow from "../window/DesktopWindow.ts";...@@ -17,6 +17,7 @@ import * as DesktopWindow from "../window/DesktopWindow.ts";
17import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts";17import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts";
18import * as DesktopEnvironment from "./DesktopEnvironment.ts";18import * as DesktopEnvironment from "./DesktopEnvironment.ts";
19import * as DesktopLifecycle from "./DesktopLifecycle.ts";19import * as DesktopLifecycle from "./DesktopLifecycle.ts";
20import * as DesktopLinuxUrlHandler from "./DesktopLinuxUrlHandler.ts";
20import * as DesktopObservability from "./DesktopObservability.ts";21import * as DesktopObservability from "./DesktopObservability.ts";
21import * as DesktopShutdown from "./DesktopShutdown.ts";22import * as DesktopShutdown from "./DesktopShutdown.ts";
22import * as DesktopServerExposure from "../backend/DesktopServerExposure.ts";23import * as DesktopServerExposure from "../backend/DesktopServerExposure.ts";
...@@ -220,6 +221,7 @@ const startup = Effect.gen(function* () {...@@ -220,6 +221,7 @@ const startup = Effect.gen(function* () {
220 const applicationMenu = yield* DesktopApplicationMenu.DesktopApplicationMenu;221 const applicationMenu = yield* DesktopApplicationMenu.DesktopApplicationMenu;
221 const electronApp = yield* ElectronApp.ElectronApp;222 const electronApp = yield* ElectronApp.ElectronApp;
222 const lifecycle = yield* DesktopLifecycle.DesktopLifecycle;223 const lifecycle = yield* DesktopLifecycle.DesktopLifecycle;
224 const linuxUrlHandler = yield* DesktopLinuxUrlHandler.DesktopLinuxUrlHandler;
223 const clerk = yield* DesktopClerk.DesktopClerk;225 const clerk = yield* DesktopClerk.DesktopClerk;
224 const shellEnvironment = yield* DesktopShellEnvironment.DesktopShellEnvironment;226 const shellEnvironment = yield* DesktopShellEnvironment.DesktopShellEnvironment;
225 const desktopSettings = yield* DesktopAppSettings.DesktopAppSettings;227 const desktopSettings = yield* DesktopAppSettings.DesktopAppSettings;
...@@ -248,6 +250,7 @@ const startup = Effect.gen(function* () {...@@ -248,6 +250,7 @@ const startup = Effect.gen(function* () {
248 yield* appIdentity.configure;250 yield* appIdentity.configure;
249 yield* applicationMenu.configure;251 yield* applicationMenu.configure;
250 yield* updates.configure;252 yield* updates.configure;
253 yield* linuxUrlHandler.register;
251 yield* bootstrap.pipe(Effect.catchCause((cause) => fatalStartupCause("bootstrap", cause)));254 yield* bootstrap.pipe(Effect.catchCause((cause) => fatalStartupCause("bootstrap", cause)));
252}).pipe(Effect.withSpan("desktop.startup"));255}).pipe(Effect.withSpan("desktop.startup"));
253256
apps/desktop/src/app/DesktopConfig.ts+1
...@@ -35,6 +35,7 @@ const compactEnv = (env: Readonly<Record<string, string | undefined>>): Record<s...@@ -35,6 +35,7 @@ const compactEnv = (env: Readonly<Record<string, string | undefined>>): Record<s
35export const DesktopConfig = Config.all({35export const DesktopConfig = Config.all({
36 appDataDirectory: trimmedString("APPDATA"),36 appDataDirectory: trimmedString("APPDATA"),
37 xdgConfigHome: trimmedString("XDG_CONFIG_HOME"),37 xdgConfigHome: trimmedString("XDG_CONFIG_HOME"),
38 xdgDataHome: trimmedString("XDG_DATA_HOME"),
38 t3Home: trimmedString("T3CODE_HOME"),39 t3Home: trimmedString("T3CODE_HOME"),
39 devServerUrl: Config.url("VITE_DEV_SERVER_URL").pipe(Config.option),40 devServerUrl: Config.url("VITE_DEV_SERVER_URL").pipe(Config.option),
40 appUserModelIdOverride: trimmedString("T3CODE_DESKTOP_APP_USER_MODEL_ID"),41 appUserModelIdOverride: trimmedString("T3CODE_DESKTOP_APP_USER_MODEL_ID"),
apps/desktop/src/app/DesktopEnvironment.ts+8
...@@ -66,6 +66,8 @@ export class DesktopEnvironment extends Context.Service<...@@ -66,6 +66,8 @@ export class DesktopEnvironment extends Context.Service<
66 readonly appUserModelId: string;66 readonly appUserModelId: string;
67 readonly linuxDesktopEntryName: string;67 readonly linuxDesktopEntryName: string;
68 readonly linuxWmClass: string;68 readonly linuxWmClass: string;
69 readonly linuxApplicationsDir: string;
70 readonly appImagePath: Option.Option<string>;
69 readonly userDataDirName: string;71 readonly userDataDirName: string;
70 readonly legacyUserDataDirName: string;72 readonly legacyUserDataDirName: string;
71 readonly defaultDesktopSettings: DesktopAppSettings.DesktopSettings;73 readonly defaultDesktopSettings: DesktopAppSettings.DesktopSettings;
...@@ -162,6 +164,10 @@ const make = Effect.fn("desktop.environment.make")(function* (...@@ -162,6 +164,10 @@ const make = Effect.fn("desktop.environment.make")(function* (
162 );164 );
163 const userDataDirName = isDevelopment ? "t3code-dev" : "t3code";165 const userDataDirName = isDevelopment ? "t3code-dev" : "t3code";
164 const legacyUserDataDirName = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";166 const legacyUserDataDirName = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";
167 const linuxApplicationsDir = path.join(
168 Option.getOrElse(config.xdgDataHome, () => path.join(homeDirectory, ".local", "share")),
169 "applications",
170 );
165 const resourcesPath = input.resourcesPath;171 const resourcesPath = input.resourcesPath;
166172
167 return DesktopEnvironment.of({173 return DesktopEnvironment.of({
...@@ -205,6 +211,8 @@ const make = Effect.fn("desktop.environment.make")(function* (...@@ -205,6 +211,8 @@ const make = Effect.fn("desktop.environment.make")(function* (
205 ),211 ),
206 linuxDesktopEntryName: isDevelopment ? "t3code-dev.desktop" : "t3code.desktop",212 linuxDesktopEntryName: isDevelopment ? "t3code-dev.desktop" : "t3code.desktop",
207 linuxWmClass: isDevelopment ? "t3code-dev" : "t3code",213 linuxWmClass: isDevelopment ? "t3code-dev" : "t3code",
214 linuxApplicationsDir,
215 appImagePath: config.appImagePath,
208 userDataDirName,216 userDataDirName,
209 legacyUserDataDirName,217 legacyUserDataDirName,
210 defaultDesktopSettings: DesktopAppSettings.resolveDefaultDesktopSettings(input.appVersion),218 defaultDesktopSettings: DesktopAppSettings.resolveDefaultDesktopSettings(input.appVersion),
apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts created+229
...@@ -0,0 +1,229 @@
1import { assert, describe, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6import * as PlatformError from "effect/PlatformError";
7import * as Sink from "effect/Sink";
8import * as Stream from "effect/Stream";
9import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
10
11import * as DesktopEnvironment from "./DesktopEnvironment.ts";
12import * as DesktopLinuxUrlHandler from "./DesktopLinuxUrlHandler.ts";
13
14interface RecordedRegistration {
15 readonly directories: string[];
16 readonly files: Array<{ readonly path: string; readonly content: string }>;
17 readonly commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }>;
18}
19
20const makeEnvironment = (overrides: Record<string, unknown> = {}) =>
21 DesktopEnvironment.DesktopEnvironment.of({
22 platform: "linux",
23 isPackaged: true,
24 isDevelopment: false,
25 displayName: "T3 Code (Alpha)",
26 linuxWmClass: "t3code",
27 linuxApplicationsDir: "/home/alice/.local/share/applications",
28 appImagePath: Option.some("/home/alice/Applications/T3-Code.AppImage"),
29 path: { join: (...parts: ReadonlyArray<string>) => parts.join("/") },
30 ...overrides,
31 } as unknown as DesktopEnvironment.DesktopEnvironment["Service"]);
32
33const mockProcess = (exitCode: number) =>
34 ChildProcessSpawner.makeHandle({
35 pid: ChildProcessSpawner.ProcessId(1),
36 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(exitCode)),
37 isRunning: Effect.succeed(false),
38 kill: () => Effect.void,
39 unref: Effect.succeed(Effect.void),
40 stdin: Sink.drain,
41 stdout: Stream.empty,
42 stderr: Stream.empty,
43 all: Stream.empty,
44 getInputFd: () => Sink.drain,
45 getOutputFd: () => Stream.empty,
46 });
47
48const makeHandlerLayer = (
49 recorded: RecordedRegistration,
50 input: {
51 readonly environment?: Record<string, unknown>;
52 readonly xdgMimeExitCode?: number;
53 readonly writeError?: PlatformError.PlatformError;
54 } = {},
55) =>
56 DesktopLinuxUrlHandler.layer.pipe(
57 Layer.provide(
58 Layer.mergeAll(
59 Layer.succeed(DesktopEnvironment.DesktopEnvironment, makeEnvironment(input.environment)),
60 FileSystem.layerNoop({
61 makeDirectory: (path) =>
62 Effect.sync(() => {
63 recorded.directories.push(path);
64 }),
65 writeFileString: (path, content) =>
66 input.writeError
67 ? Effect.fail(input.writeError)
68 : Effect.sync(() => {
69 recorded.files.push({ path, content });
70 }),
71 }),
72 Layer.succeed(
73 ChildProcessSpawner.ChildProcessSpawner,
74 ChildProcessSpawner.make((command) => {
75 const childProcess = command as unknown as {
76 readonly command: string;
77 readonly args: ReadonlyArray<string>;
78 };
79 recorded.commands.push({
80 command: childProcess.command,
81 args: childProcess.args,
82 });
83 return Effect.succeed(mockProcess(input.xdgMimeExitCode ?? 0));
84 }),
85 ),
86 ),
87 ),
88 );
89
90const runRegister = (
91 recorded: RecordedRegistration,
92 input: Parameters<typeof makeHandlerLayer>[1] = {},
93) =>
94 Effect.gen(function* () {
95 const handler = yield* DesktopLinuxUrlHandler.DesktopLinuxUrlHandler;
96 yield* handler.register;
97 }).pipe(Effect.provide(makeHandlerLayer(recorded, input)));
98
99const emptyRecording = (): RecordedRegistration => ({
100 directories: [],
101 files: [],
102 commands: [],
103});
104
105describe("DesktopLinuxUrlHandler", () => {
106 it("renders a scheme-handler desktop entry with freedesktop Exec quoting", () => {
107 const entry = DesktopLinuxUrlHandler.renderUrlHandlerDesktopEntry({
108 displayName: "T3 Code (Nightly)",
109 execTarget: '/home/al ice/Apps/T3 "100%" $HOME\\x.AppImage',
110 scheme: "t3code",
111 });
112
113 assert.include(entry, "[Desktop Entry]");
114 assert.include(entry, "Name=T3 Code (Nightly)");
115 // Exec composes both escaping layers: a literal backslash becomes four
116 // backslashes in the file, a quote three characters, a dollar sign two
117 // backslashes plus the sign.
118 assert.include(
119 entry,
120 'Exec="/home/al ice/Apps/T3 \\\\"100%%\\\\" \\\\$HOME\\\\\\\\x.AppImage" %U',
121 );
122 assert.include(entry, "NoDisplay=true");
123 assert.notInclude(entry, "StartupWMClass=");
124 assert.include(entry, "MimeType=x-scheme-handler/t3code;");
125 });
126
127 it("carries structured context on registration errors", () => {
128 const writeError = new DesktopLinuxUrlHandler.DesktopLinuxUrlHandlerRegistrationError({
129 step: "write-desktop-entry",
130 scheme: "t3code",
131 desktopEntryPath: "/home/alice/.local/share/applications/t3code-url-handler.desktop",
132 cause: new Error("boom"),
133 });
134 assert.equal(
135 writeError.message,
136 "Failed to register the t3code:// URL handler (step: write-desktop-entry).",
137 );
138 assert.equal(
139 writeError.desktopEntryPath,
140 "/home/alice/.local/share/applications/t3code-url-handler.desktop",
141 );
142
143 const exitError = new DesktopLinuxUrlHandler.DesktopLinuxUrlHandlerRegistrationError({
144 step: "set-default-handler",
145 scheme: "t3code",
146 exitCode: 4,
147 });
148 assert.equal(
149 exitError.message,
150 "Failed to register the t3code:// URL handler (step: set-default-handler, xdg-mime exit code 4).",
151 );
152 });
153
154 it.effect("writes the handler entry and claims the scheme default via xdg-mime", () => {
155 const recorded = emptyRecording();
156
157 return Effect.gen(function* () {
158 yield* runRegister(recorded);
159
160 assert.deepEqual(recorded.directories, ["/home/alice/.local/share/applications"]);
161 assert.equal(recorded.files.length, 1);
162 assert.equal(
163 recorded.files[0]?.path,
164 "/home/alice/.local/share/applications/t3code-url-handler.desktop",
165 );
166 assert.include(
167 recorded.files[0]?.content,
168 'Exec="/home/alice/Applications/T3-Code.AppImage" %U',
169 );
170 assert.include(recorded.files[0]?.content, "MimeType=x-scheme-handler/t3code;");
171 assert.deepEqual(recorded.commands, [
172 {
173 command: "xdg-mime",
174 args: ["default", "t3code-url-handler.desktop", "x-scheme-handler/t3code"],
175 },
176 ]);
177 });
178 });
179
180 it.effect("falls back to the process executable outside an AppImage", () => {
181 const recorded = emptyRecording();
182
183 return Effect.gen(function* () {
184 yield* runRegister(recorded, { environment: { appImagePath: Option.none() } });
185
186 assert.include(
187 recorded.files[0]?.content,
188 `Exec=${DesktopLinuxUrlHandler.escapeDesktopEntryExecArgument(process.execPath)} %U`,
189 );
190 });
191 });
192
193 it.effect("does nothing on other platforms or unpackaged builds", () => {
194 const nonLinux = emptyRecording();
195 const unpackaged = emptyRecording();
196
197 return Effect.gen(function* () {
198 yield* runRegister(nonLinux, { environment: { platform: "darwin" } });
199 yield* runRegister(unpackaged, { environment: { isPackaged: false } });
200
201 for (const recorded of [nonLinux, unpackaged]) {
202 assert.deepEqual(recorded.directories, []);
203 assert.deepEqual(recorded.files, []);
204 assert.deepEqual(recorded.commands, []);
205 }
206 });
207 });
208
209 it.effect("never fails startup when registration cannot complete", () => {
210 const xdgMimeFailed = emptyRecording();
211 const writeFailed = emptyRecording();
212
213 return Effect.gen(function* () {
214 yield* runRegister(xdgMimeFailed, { xdgMimeExitCode: 1 });
215 yield* runRegister(writeFailed, {
216 writeError: PlatformError.systemError({
217 _tag: "PermissionDenied",
218 module: "FileSystem",
219 method: "writeFileString",
220 description: "read-only filesystem",
221 pathOrDescriptor: "/home/alice/.local/share/applications/t3code-url-handler.desktop",
222 }),
223 });
224
225 assert.equal(xdgMimeFailed.files.length, 1);
226 assert.deepEqual(writeFailed.commands, []);
227 });
228 });
229});
apps/desktop/src/app/DesktopLinuxUrlHandler.ts created+191
...@@ -0,0 +1,191 @@
1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6import * as Schema from "effect/Schema";
7import * as ChildProcess from "effect/unstable/process/ChildProcess";
8import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
9
10import * as ElectronProtocol from "../electron/ElectronProtocol.ts";
11import * as DesktopEnvironment from "./DesktopEnvironment.ts";
12import { makeComponentLogger } from "./DesktopObservability.ts";
13
14// Linux ships as an AppImage, so the .desktop entry users end up with is
15// created by whatever integration tool they use (AppImageLauncher names it
16// appimagekit_<hash>-….desktop) and its filename is not under our control.
17// Electron's app.setAsDefaultProtocolClient resolves the desktop id from
18// setDesktopName, which cannot match those files — so the browser keeps
19// prompting "Choose an application" for every OAuth callback. Instead, write
20// our own handler entry pointing at the current AppImage and claim the
21// scheme default via xdg-mime, exactly what the file manager's "set as
22// default" checkbox would record in mimeapps.list.
23export const URL_HANDLER_DESKTOP_ENTRY_NAME = "t3code-url-handler.desktop";
24
25const { logInfo, logWarning } = makeComponentLogger("desktop-linux-url-handler");
26
27export class DesktopLinuxUrlHandlerRegistrationError extends Schema.TaggedErrorClass<DesktopLinuxUrlHandlerRegistrationError>()(
28 "DesktopLinuxUrlHandlerRegistrationError",
29 {
30 step: Schema.Literals(["write-desktop-entry", "set-default-handler"]),
31 scheme: Schema.String,
32 desktopEntryPath: Schema.optionalKey(Schema.String),
33 exitCode: Schema.optionalKey(Schema.Number),
34 cause: Schema.optionalKey(Schema.Defect()),
35 },
36) {
37 override get message(): string {
38 const exitCode = this.exitCode === undefined ? "" : `, xdg-mime exit code ${this.exitCode}`;
39 return `Failed to register the ${this.scheme}:// URL handler (step: ${this.step}${exitCode}).`;
40 }
41}
42
43const isRegistrationError = Schema.is(DesktopLinuxUrlHandlerRegistrationError);
44
45const escapeDesktopEntryString = (value: string): string =>
46 value
47 .replaceAll("\\", "\\\\")
48 .replaceAll("\n", "\\n")
49 .replaceAll("\r", "\\r")
50 .replaceAll("\t", "\\t");
51
52// Exec values are unescaped twice by implementations: first the general
53// string-value rules, then the Exec quoting rules — so writing composes the
54// layers in reverse. The argument is double-quoted with reserved characters
55// backslash-escaped and literal percent signs doubled (field codes), and the
56// general string escaping is applied on top: a literal backslash ends up as
57// four backslashes in the file, a quote as \\", a dollar sign as \\$.
58export function escapeDesktopEntryExecArgument(value: string): string {
59 const quoted = value
60 .replaceAll("\\", () => "\\\\")
61 .replaceAll("`", () => "\\`")
62 .replaceAll("$", () => "\\$")
63 .replaceAll('"', () => '\\"')
64 .replaceAll("%", () => "%%");
65 return escapeDesktopEntryString(`"${quoted}"`);
66}
67
68// The AppImage integration entry owns the window identity and icon. This
69// hidden URL-only entry must not compete with it for StartupWMClass matching.
70export function renderUrlHandlerDesktopEntry(input: {
71 readonly displayName: string;
72 readonly execTarget: string;
73 readonly scheme: string;
74}): string {
75 return [
76 "[Desktop Entry]",
77 "Type=Application",
78 `Name=${escapeDesktopEntryString(input.displayName)}`,
79 `Exec=${escapeDesktopEntryExecArgument(input.execTarget)} %U`,
80 "Terminal=false",
81 "NoDisplay=true",
82 "StartupNotify=false",
83 `MimeType=x-scheme-handler/${input.scheme};`,
84 "",
85 ].join("\n");
86}
87
88export class DesktopLinuxUrlHandler extends Context.Service<
89 DesktopLinuxUrlHandler,
90 {
91 readonly register: Effect.Effect<void>;
92 }
93>()("@t3tools/desktop/app/DesktopLinuxUrlHandler") {}
94
95export const make = Effect.gen(function* () {
96 const environment = yield* DesktopEnvironment.DesktopEnvironment;
97 const fileSystem = yield* FileSystem.FileSystem;
98 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
99
100 const scheme = ElectronProtocol.getDesktopScheme(environment.isDevelopment);
101 const desktopEntryPath = environment.path.join(
102 environment.linuxApplicationsDir,
103 URL_HANDLER_DESKTOP_ENTRY_NAME,
104 );
105
106 const writeDesktopEntry = Effect.gen(function* () {
107 // Inside the mounted AppImage, process.execPath points at a transient
108 // /tmp/.mount_* path — the handler must launch the AppImage itself.
109 const execTarget = Option.getOrElse(environment.appImagePath, () => process.execPath);
110 yield* fileSystem.makeDirectory(environment.linuxApplicationsDir, { recursive: true });
111 yield* fileSystem.writeFileString(
112 desktopEntryPath,
113 renderUrlHandlerDesktopEntry({
114 displayName: environment.displayName,
115 execTarget,
116 scheme,
117 }),
118 );
119 }).pipe(
120 Effect.mapError(
121 (cause) =>
122 new DesktopLinuxUrlHandlerRegistrationError({
123 step: "write-desktop-entry",
124 scheme,
125 desktopEntryPath,
126 cause,
127 }),
128 ),
129 );
130
131 const setDefaultHandler = Effect.scoped(
132 Effect.gen(function* () {
133 const command = ChildProcess.make(
134 "xdg-mime",
135 ["default", URL_HANDLER_DESKTOP_ENTRY_NAME, `x-scheme-handler/${scheme}`],
136 {
137 stdin: "ignore",
138 stdout: "ignore",
139 stderr: "ignore",
140 },
141 );
142 const handle = yield* spawner.spawn(command);
143 const exitCode = yield* handle.exitCode;
144 if ((exitCode as unknown as number) !== 0) {
145 return yield* new DesktopLinuxUrlHandlerRegistrationError({
146 step: "set-default-handler",
147 scheme,
148 exitCode: Number(exitCode),
149 });
150 }
151 }),
152 ).pipe(
153 Effect.mapError((error) =>
154 isRegistrationError(error)
155 ? error
156 : new DesktopLinuxUrlHandlerRegistrationError({
157 step: "set-default-handler",
158 scheme,
159 cause: error,
160 }),
161 ),
162 );
163
164 const register = Effect.gen(function* () {
165 if (environment.platform !== "linux" || !environment.isPackaged) {
166 return;
167 }
168 yield* writeDesktopEntry;
169 yield* setDefaultHandler;
170 yield* logInfo("registered URL scheme handler", { scheme });
171 }).pipe(
172 // Registration is best-effort: a missing xdg-mime or read-only home must
173 // never block startup — the OS chooser remains as fallback.
174 Effect.catch((error) =>
175 logWarning("URL scheme handler registration failed", {
176 scheme,
177 step: error.step,
178 message: error.message,
179 ...(error.desktopEntryPath === undefined
180 ? {}
181 : { desktopEntryPath: error.desktopEntryPath }),
182 ...(error.exitCode === undefined ? {} : { exitCode: error.exitCode }),
183 }),
184 ),
185 Effect.withSpan("desktop.linuxUrlHandler.register"),
186 );
187
188 return DesktopLinuxUrlHandler.of({ register });
189});
190
191export const layer = Layer.effect(DesktopLinuxUrlHandler, make);
apps/desktop/src/main.ts+2
...@@ -43,6 +43,7 @@ import * as DesktopLocalEnvironmentAuth from "./backend/DesktopLocalEnvironmentA...@@ -43,6 +43,7 @@ import * as DesktopLocalEnvironmentAuth from "./backend/DesktopLocalEnvironmentA
43import * as DesktopNetworkInterfaces from "./backend/DesktopNetworkInterfaces.ts";43import * as DesktopNetworkInterfaces from "./backend/DesktopNetworkInterfaces.ts";
44import * as DesktopEnvironment from "./app/DesktopEnvironment.ts";44import * as DesktopEnvironment from "./app/DesktopEnvironment.ts";
45import * as DesktopLifecycle from "./app/DesktopLifecycle.ts";45import * as DesktopLifecycle from "./app/DesktopLifecycle.ts";
46import * as DesktopLinuxUrlHandler from "./app/DesktopLinuxUrlHandler.ts";
46import * as DesktopShutdown from "./app/DesktopShutdown.ts";47import * as DesktopShutdown from "./app/DesktopShutdown.ts";
47import * as DesktopObservability from "./app/DesktopObservability.ts";48import * as DesktopObservability from "./app/DesktopObservability.ts";
48import * as DesktopServerExposure from "./backend/DesktopServerExposure.ts";49import * as DesktopServerExposure from "./backend/DesktopServerExposure.ts";
...@@ -181,6 +182,7 @@ const desktopLocalEnvironmentAuthLayer = DesktopLocalEnvironmentAuth.layer.pipe(...@@ -181,6 +182,7 @@ const desktopLocalEnvironmentAuthLayer = DesktopLocalEnvironmentAuth.layer.pipe(
181const desktopApplicationLayer = Layer.mergeAll(182const desktopApplicationLayer = Layer.mergeAll(
182 DesktopLifecycle.layer,183 DesktopLifecycle.layer,
183 DesktopApplicationMenu.layer,184 DesktopApplicationMenu.layer,
185 DesktopLinuxUrlHandler.layer,
184 DesktopShellEnvironment.layer,186 DesktopShellEnvironment.layer,
185 desktopSshLayer,187 desktopSshLayer,
186).pipe(188).pipe(