Skip to content
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
aa9dd25
Port desktop backend readiness checks to Effect
juliusmarminge May 6, 2026
4d3358e
Fix timeout silently succeeding in waitForHttpReadyEffect
cursoragent May 6, 2026
167afc1
nit
juliusmarminge May 6, 2026
f5a6173
Refactor desktop backend startup into Effect process
juliusmarminge May 6, 2026
91c982f
Refactor desktop runtime to Effect
juliusmarminge May 6, 2026
f9000d4
Refactor desktop shell env sync into Effect service
juliusmarminge May 6, 2026
9e2d64c
Refactor desktop IPC into shared handlers
juliusmarminge May 6, 2026
0f5850d
Refactor desktop SSH IPC handlers
juliusmarminge May 6, 2026
8e162b5
Centralize desktop window and quitting state
juliusmarminge May 6, 2026
adfeee0
Refactor desktop Electron IPC into shared services
juliusmarminge May 6, 2026
5461149
Refactor desktop Electron services
juliusmarminge May 6, 2026
a90619e
Refactor desktop update handling into dedicated services
juliusmarminge May 6, 2026
d9435a0
Refactor desktop server exposure into scoped service
juliusmarminge May 6, 2026
3f82d86
Split desktop SSH handling into dedicated services
juliusmarminge May 6, 2026
0f9fb93
Refactor desktop window, theme, and updater services
juliusmarminge May 7, 2026
4ecf54a
Refactor desktop app into main-layer modules
juliusmarminge May 7, 2026
ac4ff5e
Refactor desktop IPC to use main service layers
juliusmarminge May 7, 2026
a8b9c4b
Use default update channel for desktop update filtering
juliusmarminge May 7, 2026
a122a35
Namespace preload IPC channel imports
juliusmarminge May 7, 2026
f0c204d
Refactor desktop IPC methods to use channel namespace imports
juliusmarminge May 7, 2026
e4c0d77
Reorganize desktop app modules by domain
juliusmarminge May 7, 2026
9fb7747
Use desktop server exposure domain folder
juliusmarminge May 7, 2026
5a3e6b1
Refine desktop error handling and Effect imports
juliusmarminge May 7, 2026
ffc7c77
Refactor contracts to use Effect subpath imports
juliusmarminge May 7, 2026
244e066
Normalize Effect imports across shared packages
juliusmarminge May 7, 2026
f854da1
Reuse effect context for SSH password prompt cleanup
juliusmarminge May 7, 2026
b07e626
Tighten Effect tsconfig diagnostics
juliusmarminge May 7, 2026
efa529d
Refactor desktop bootstrap around direct environment inputs
juliusmarminge May 7, 2026
a59ee63
Handle desktop secret decode and window load errors
juliusmarminge May 7, 2026
ec26ca7
Inline SSH password cancel IPC result
juliusmarminge May 7, 2026
354c05f
Normalize SSH password prompt cancelled IPC result
juliusmarminge May 7, 2026
02fa783
Harden desktop backend restart and update handling
juliusmarminge May 7, 2026
0ee84dc
Refactor desktop settings into dedicated services
juliusmarminge May 7, 2026
7bdfc8c
Fix rebased desktop bootstrap config
juliusmarminge May 7, 2026
ccee7ce
Simplify desktop shutdown and timeout handling
juliusmarminge May 7, 2026
daf956b
Refactor desktop logging around run annotations
juliusmarminge May 7, 2026
d008e09
Probe backend readiness on environment endpoint
juliusmarminge May 7, 2026
413d2f9
Improve desktop startup logging and window readiness
juliusmarminge May 7, 2026
fe6d0ee
Consolidate desktop and server observability layers
juliusmarminge May 8, 2026
79b3420
Cancel desktop backend restart when stopped
juliusmarminge May 8, 2026
6937dab
Add span tracing to desktop startup and lifecycle
juliusmarminge May 8, 2026
e8344e5
Merge origin/main into desktop effect rewrite
juliusmarminge May 8, 2026
dc3aa27
Skip destroyed Electron windows during appearance sync
juliusmarminge May 8, 2026
35835a7
Unify desktop component logging
juliusmarminge May 8, 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/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gitignore patterns now overly broad across monorepo

Low Severity

Moving dist-electron/ and .electron-runtime/ from apps/desktop/.gitignore to the root .gitignore broadens matching scope from just apps/desktop/ to any directory at any depth in the monorepo. Gitignore patterns without a leading path separator match anywhere in the tree. Since this is a monorepo with multiple apps/packages, any future package that happens to use a dist-electron/ directory would have it silently ignored.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 01e1673. Configure here.

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 Random from "effect/Random";
import * as Ref from "effect/Ref";

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 DesktopServerExposure from "../serverExposure/DesktopServerExposure.ts";
import * as DesktopAppSettings from "../settings/DesktopAppSettings.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;

const makeDesktopRunId = Random.nextUUIDv4.pipe(
Effect.map((value) => value.replaceAll("-", "").slice(0, 12)),
);

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
| 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 message = error instanceof Error ? error.message : String(error);
const detail =
error instanceof Error && typeof error.stack === "string" ? `\n${error.stack}` : "";
yield* Effect.logError("fatal startup error").pipe(
Effect.annotateLogs({
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 desktopSettings = yield* DesktopAppSettings.DesktopAppSettings;
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
yield* Effect.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* Effect.logInfo(
backendPortSelection.selectedByScan
? "selected backend port via sequential scan"
: "using configured backend port",
).pipe(
Effect.annotateLogs({
port: backendPort,
...(backendPortSelection.selectedByScan ? { startPort: DEFAULT_DESKTOP_BACKEND_PORT } : {}),
}),
);

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

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

if (!(yield* Ref.get(state.quitting))) {
yield* backendManager.start;
}
yield* Effect.logInfo("bootstrap backend start requested");
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

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

export const program = Effect.scoped(
Effect.gen(function* () {
const runId = yield* makeDesktopRunId;
yield* Effect.annotateLogsScoped({ scope: "desktop", runId });

const shutdown = yield* DesktopLifecycle.DesktopShutdown;
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 desktopSettings = yield* DesktopAppSettings.DesktopAppSettings;
const updates = yield* DesktopUpdates.DesktopUpdates;
const environment = yield* DesktopEnvironment.DesktopEnvironment;

yield* electronProtocol.registerDesktopSchemePrivileges;
yield* Effect.addFinalizer(() =>
backendManager.stop().pipe(Effect.ensuring(shutdown.markComplete)),
);

yield* shellEnvironment.installIntoProcess;
const userDataPath = yield* appIdentity.resolveUserDataPath;
yield* electronApp.setPath("userData", userDataPath);
yield* Effect.logInfo("runtime logging configured").pipe(
Effect.annotateLogs({ logDir: environment.logDir }),
);
yield* desktopSettings.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* Effect.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