Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ac1b470
Port desktop backend readiness checks to Effect
juliusmarminge May 6, 2026
63f1bdf
Fix timeout silently succeeding in waitForHttpReadyEffect
cursoragent May 6, 2026
d8f0d72
nit
juliusmarminge May 6, 2026
b5c8ea4
Refactor desktop backend startup into Effect process
juliusmarminge May 6, 2026
add1b6f
Refactor desktop runtime to Effect
juliusmarminge May 6, 2026
3a55a1d
Refactor desktop shell env sync into Effect service
juliusmarminge May 6, 2026
6d22abb
Refactor desktop IPC into shared handlers
juliusmarminge May 6, 2026
96baace
Refactor desktop SSH IPC handlers
juliusmarminge May 6, 2026
7e0e747
Centralize desktop window and quitting state
juliusmarminge May 6, 2026
4c65474
Refactor desktop Electron IPC into shared services
juliusmarminge May 6, 2026
aa9d1f1
Refactor desktop Electron services
juliusmarminge May 6, 2026
2ce93c6
Refactor desktop update handling into dedicated services
juliusmarminge May 6, 2026
dc62ae1
Refactor desktop server exposure into scoped service
juliusmarminge May 6, 2026
0fbb143
Split desktop SSH handling into dedicated services
juliusmarminge May 6, 2026
5a2b760
Refactor desktop window, theme, and updater services
juliusmarminge May 7, 2026
aab30c6
Refactor desktop app into main-layer modules
juliusmarminge May 7, 2026
6a07b7a
Refactor desktop IPC to use main service layers
juliusmarminge May 7, 2026
cd9d43b
Use default update channel for desktop update filtering
juliusmarminge May 7, 2026
71a332e
Namespace preload IPC channel imports
juliusmarminge May 7, 2026
04cb180
Refactor desktop IPC methods to use channel namespace imports
juliusmarminge May 7, 2026
5a8bc01
Reorganize desktop app modules by domain
juliusmarminge May 7, 2026
dc9e39e
Use desktop server exposure domain folder
juliusmarminge May 7, 2026
c9345f4
Merge origin/main into desktop Effect PR
juliusmarminge May 7, 2026
178db6b
Refine desktop error handling and Effect imports
juliusmarminge May 7, 2026
49d4f44
Refactor contracts to use Effect subpath imports
juliusmarminge May 7, 2026
ac40499
Normalize Effect imports across shared packages
juliusmarminge May 7, 2026
62e25a4
Reuse effect context for SSH password prompt cleanup
juliusmarminge May 7, 2026
3947f31
Tighten Effect tsconfig diagnostics
juliusmarminge May 7, 2026
06396ce
Refactor desktop bootstrap around direct environment inputs
juliusmarminge May 7, 2026
f383026
Handle desktop secret decode and window load errors
juliusmarminge May 7, 2026
ab476a0
Inline SSH password cancel IPC result
juliusmarminge May 7, 2026
66b1ecd
Normalize SSH password prompt cancelled IPC result
juliusmarminge May 7, 2026
f57d34a
Harden desktop backend restart and update handling
juliusmarminge May 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ __screenshots__/
.tanstack
squashfs-root/
.vercel
dist-electron/
.electron-runtime/
2 changes: 0 additions & 2 deletions apps/desktop/.gitignore

This file was deleted.

2 changes: 2 additions & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"electron-updater": "^6.6.2"
},
"devDependencies": {
"@effect/language-service": "catalog:",
"@effect/vitest": "catalog:",
"@t3tools/client-runtime": "workspace:*",
"@t3tools/contracts": "workspace:*",
"@t3tools/shared": "workspace:*",
Expand Down
237 changes: 237 additions & 0 deletions apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import * as Cause from "effect/Cause";
import * as Data from "effect/Data";
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
import * as Scope from "effect/Scope";

import * as NetService from "@t3tools/shared/Net";
import * as ElectronApp from "../electron/ElectronApp.ts";
import * as ElectronDialog from "../electron/ElectronDialog.ts";
import * as ElectronProtocol from "../electron/ElectronProtocol.ts";
import { installDesktopIpcHandlers } from "../ipc/DesktopIpcHandlers.ts";
import * as DesktopAppIdentity from "./DesktopAppIdentity.ts";
import * as DesktopApplicationMenu from "../window/DesktopApplicationMenu.ts";
import * as DesktopBackendManager from "../backend/DesktopBackendManager.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
import * as DesktopLifecycle from "./DesktopLifecycle.ts";
import * as DesktopRun from "./DesktopRun.ts";
import * as DesktopServerExposure from "../serverExposure/DesktopServerExposure.ts";
import * as DesktopSettingsState from "../settings/DesktopSettingsState.ts";
import * as DesktopShellEnvironment from "../shell/DesktopShellEnvironment.ts";
import * as DesktopState from "./DesktopState.ts";
import * as DesktopUpdates from "../updates/DesktopUpdates.ts";
import * as DesktopWindow from "../window/DesktopWindow.ts";

const DEFAULT_DESKTOP_BACKEND_PORT = 3773;
const MAX_TCP_PORT = 65_535;
const DESKTOP_BACKEND_PORT_PROBE_HOSTS = ["127.0.0.1", "0.0.0.0", "::"] as const;

class DesktopBackendPortUnavailableError extends Data.TaggedError(
"DesktopBackendPortUnavailableError",
)<{
readonly startPort: number;
readonly maxPort: number;
readonly hosts: readonly string[];
}> {
override get message() {
return `No desktop backend port is available on hosts ${this.hosts.join(", ")} between ${this.startPort} and ${this.maxPort}.`;
}
}

class DesktopDevelopmentBackendPortRequiredError extends Data.TaggedError(
"DesktopDevelopmentBackendPortRequiredError",
)<{}> {
override get message() {
return "T3CODE_PORT is required in desktop development.";
}
}

const resolveDesktopBackendPort = Effect.fn("resolveDesktopBackendPort")(function* (
configuredPort: Option.Option<number>,
) {
if (Option.isSome(configuredPort)) {
return {
port: configuredPort.value,
selectedByScan: false,
} as const;
}

const net = yield* NetService.NetService;
for (let port = DEFAULT_DESKTOP_BACKEND_PORT; port <= MAX_TCP_PORT; port += 1) {
let availableOnEveryHost = true;

for (const host of DESKTOP_BACKEND_PORT_PROBE_HOSTS) {
if (!(yield* net.canListenOnHost(port, host))) {
availableOnEveryHost = false;
break;
}
}

if (availableOnEveryHost) {
return {
port,
selectedByScan: true,
} as const;
}
}

return yield* new DesktopBackendPortUnavailableError({
startPort: DEFAULT_DESKTOP_BACKEND_PORT,
maxPort: MAX_TCP_PORT,
hosts: DESKTOP_BACKEND_PORT_PROBE_HOSTS,
});
});

const handleFatalStartupError = (
stage: string,
error: unknown,
): Effect.Effect<
void,
never,
| DesktopLifecycle.DesktopShutdown
| DesktopRun.DesktopRun
| DesktopState.DesktopState
| ElectronApp.ElectronApp
| ElectronDialog.ElectronDialog
> =>
Effect.gen(function* () {
const shutdown = yield* DesktopLifecycle.DesktopShutdown;
const state = yield* DesktopState.DesktopState;
const electronApp = yield* ElectronApp.ElectronApp;
const electronDialog = yield* ElectronDialog.ElectronDialog;
const run = yield* DesktopRun.DesktopRun;
const message = error instanceof Error ? error.message : String(error);
const detail =
error instanceof Error && typeof error.stack === "string" ? `\n${error.stack}` : "";
yield* run.logError("fatal startup error", {
stage,
message,
...(detail.length > 0 ? { detail } : {}),
});
const wasQuitting = yield* Ref.getAndSet(state.quitting, true);
if (!wasQuitting) {
yield* electronDialog.showErrorBox(
"T3 Code failed to start",
`Stage: ${stage}\n${message}${detail}`,
);
}
yield* shutdown.request;
yield* electronApp.quit;
});

const fatalStartupCause = <E>(stage: string, cause: Cause.Cause<E>) =>
handleFatalStartupError(stage, Cause.pretty(cause)).pipe(Effect.andThen(Effect.failCause(cause)));

const bootstrap = Effect.gen(function* () {
const backendManager = yield* DesktopBackendManager.DesktopBackendManager;
const state = yield* DesktopState.DesktopState;
const desktopWindow = yield* DesktopWindow.DesktopWindow;
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const settingsState = yield* DesktopSettingsState.DesktopSettingsState;
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
const run = yield* DesktopRun.DesktopRun;
yield* run.logInfo("bootstrap start");

if (environment.isDevelopment && Option.isNone(environment.configuredBackendPort)) {
return yield* new DesktopDevelopmentBackendPortRequiredError();
}

const backendPortSelection = yield* resolveDesktopBackendPort(environment.configuredBackendPort);
const backendPort = backendPortSelection.port;
yield* run.logInfo(
backendPortSelection.selectedByScan
? "selected backend port via sequential scan"
: "using configured backend port",
{
port: backendPort,
...(backendPortSelection.selectedByScan ? { startPort: DEFAULT_DESKTOP_BACKEND_PORT } : {}),
},
);

const settings = yield* settingsState.get;
if (settings.serverExposureMode !== environment.defaultDesktopSettings.serverExposureMode) {
yield* run.logInfo("bootstrap restoring persisted server exposure mode", {
mode: settings.serverExposureMode,
});
}
const serverExposureState = yield* serverExposure.configureFromSettings({ port: backendPort });
const backendConfig = yield* serverExposure.backendConfig;
yield* run.logInfo("bootstrap resolved backend endpoint", {
baseUrl: backendConfig.httpBaseUrl.href,
});
if (serverExposureState.endpointUrl) {
yield* run.logInfo("bootstrap enabled network access", {
endpointUrl: serverExposureState.endpointUrl,
});
} else if (settings.serverExposureMode === "network-accessible") {
yield* run.logWarning(
"bootstrap fell back to local-only because no advertised network host was available",
);
}

yield* installDesktopIpcHandlers;
yield* run.logInfo("bootstrap ipc handlers registered");

if (!(yield* Ref.get(state.quitting))) {
yield* backendManager.start;
}
yield* run.logInfo("bootstrap backend start requested");

if (environment.isDevelopment) {
yield* desktopWindow.ensureMain;
}
});

export const program = Effect.scoped(
Effect.gen(function* () {
const shutdown = yield* DesktopLifecycle.DesktopShutdown;

yield* Effect.gen(function* () {
const appIdentity = yield* DesktopAppIdentity.DesktopAppIdentity;
const applicationMenu = yield* DesktopApplicationMenu.DesktopApplicationMenu;
const backendManager = yield* DesktopBackendManager.DesktopBackendManager;
const electronApp = yield* ElectronApp.ElectronApp;
const electronProtocol = yield* ElectronProtocol.ElectronProtocol;
const lifecycle = yield* DesktopLifecycle.DesktopLifecycle;
const shellEnvironment = yield* DesktopShellEnvironment.DesktopShellEnvironment;
const settingsState = yield* DesktopSettingsState.DesktopSettingsState;
const updates = yield* DesktopUpdates.DesktopUpdates;
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const run = yield* DesktopRun.DesktopRun;

yield* electronProtocol.registerDesktopSchemePrivileges;
yield* run.refreshId;
yield* Scope.addFinalizer(
yield* Scope.Scope,
Effect.zip(backendManager.shutdown, updates.shutdown).pipe(
Effect.ensuring(shutdown.markComplete),
),
);

yield* shellEnvironment.installIntoProcess;
const userDataPath = yield* appIdentity.resolveUserDataPath;
yield* electronApp.setPath("userData", userDataPath);
yield* run.logInfo("runtime logging configured", { logDir: environment.logDir });
yield* settingsState.load;

if (environment.platform === "linux") {
yield* electronApp.appendCommandLineSwitch("class", environment.linuxWmClass);
}

yield* appIdentity.configure;
yield* lifecycle.register;

yield* electronApp.whenReady.pipe(
Effect.catchCause((cause) => fatalStartupCause("whenReady", cause)),
);
yield* run.logInfo("app ready");
yield* appIdentity.configure;
yield* applicationMenu.configure;
yield* electronProtocol.registerDesktopFileProtocol;
yield* updates.configure;
yield* bootstrap.pipe(Effect.catchCause((cause) => fatalStartupCause("bootstrap", cause)));
yield* shutdown.awaitRequest;
});
}),
);
Loading
Loading