port desktop app to Effect#2546
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Timeout silently succeeds instead of failing with error
- Added Option.match after Effect.timeoutOption to convert Option.None (timeout) into a BackendTimeoutError failure instead of silently succeeding, matching the established pattern used in all other timeoutOption call sites in the codebase.
Or push these changes by commenting:
@cursor push 27f2b65c9b
Preview (27f2b65c9b)
diff --git a/apps/desktop/src/backendReadiness.ts b/apps/desktop/src/backendReadiness.ts
--- a/apps/desktop/src/backendReadiness.ts
+++ b/apps/desktop/src/backendReadiness.ts
@@ -3,6 +3,7 @@
import * as Data from "effect/Data";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
+import * as Option from "effect/Option";
import * as Predicate from "effect/Predicate";
import * as Schedule from "effect/Schedule";
import { HttpClient } from "effect/unstable/http";
@@ -60,9 +61,13 @@
yield* client.get(requestUrl).pipe(
Effect.asVoid,
Effect.timeoutOption(timeout),
+ Effect.flatMap(
+ Option.match({
+ onNone: () => Effect.fail(new BackendTimeoutError({ url: baseUrl })),
+ onSome: () => Effect.void,
+ }),
+ ),
Effect.catchTags({
- TimeoutError: () => Effect.fail(new BackendTimeoutError({ url: baseUrl })),
- // Maybe map this to different error kind?
HttpClientError: () => Effect.fail(new BackendTimeoutError({ url: baseUrl })),
}),
);You can send follow-ups to the cloud agent here.
ApprovabilityVerdict: Needs human review Diff is too large for automated approval analysis. A human reviewer should evaluate this PR. You can customize Macroscope's approvability policy. Learn more. |
Dismissing prior approval to re-evaluate d8f0d72
Dismissing prior approval to re-evaluate b5c8ea4
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Port fields lose range validation in bootstrap schema
- Replaced Schema.Number with a shared PortSchema (Schema.Int with isBetween 1-65535) for both port and tailscaleServePort fields in DesktopBackendBootstrap, restoring the validation that was present in the old BootstrapEnvelopeSchema.
- ✅ Fixed: Readiness timeout fires but no window ever created
- Added fallback window creation in the onReadinessFailure handler so users get a window (showing reconnection state) instead of being stuck with no UI when the readiness probe times out.
Or push these changes by commenting:
@cursor push 1291f41e07
Preview (1291f41e07)
diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts
--- a/apps/desktop/src/main.ts
+++ b/apps/desktop/src/main.ts
@@ -1496,6 +1496,12 @@
`bootstrap backend readiness warning message=${formatErrorMessage(error)}`,
);
console.warn("[desktop] backend readiness check failed during bootstrap", error);
+
+ if (isDevelopment) return;
+ const existingWindow = mainWindow ?? BrowserWindow.getAllWindows()[0] ?? null;
+ if (existingWindow !== null) return;
+ mainWindow = createWindow();
+ writeDesktopLogHeader("bootstrap main window created (readiness timeout fallback)");
}),
onOutput: (streamName, chunk) =>
Effect.sync(() => {
diff --git a/apps/server/src/cli.ts b/apps/server/src/cli.ts
--- a/apps/server/src/cli.ts
+++ b/apps/server/src/cli.ts
@@ -5,6 +5,7 @@
CommandId,
DesktopBackendBootstrap,
OrchestrationReadModel,
+ PortSchema,
ProjectId,
type ClientOrchestrationCommand,
} from "@t3tools/contracts";
@@ -67,8 +68,6 @@
import { WorkspacePaths } from "./workspace/Services/WorkspacePaths.ts";
import { WorkspacePathsLive } from "./workspace/Layers/WorkspacePaths.ts";
-const PortSchema = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }));
-
const modeFlag = Flag.choice("mode", RuntimeMode.literals).pipe(
Flag.withDescription("Runtime mode. `desktop` keeps loopback defaults unless overridden."),
Flag.optional,
diff --git a/packages/contracts/src/baseSchemas.ts b/packages/contracts/src/baseSchemas.ts
--- a/packages/contracts/src/baseSchemas.ts
+++ b/packages/contracts/src/baseSchemas.ts
@@ -5,6 +5,7 @@
export const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
export const PositiveInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1));
+export const PortSchema = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }));
export const IsoDateTime = Schema.String;
export type IsoDateTime = typeof IsoDateTime.Type;
diff --git a/packages/contracts/src/desktopBootstrap.ts b/packages/contracts/src/desktopBootstrap.ts
--- a/packages/contracts/src/desktopBootstrap.ts
+++ b/packages/contracts/src/desktopBootstrap.ts
@@ -1,14 +1,16 @@
import { Schema } from "effect";
+import { PortSchema } from "./baseSchemas.ts";
+
export const DesktopBackendBootstrap = Schema.Struct({
mode: Schema.Literal("desktop"),
noBrowser: Schema.Boolean,
- port: Schema.Number,
+ port: PortSchema,
t3Home: Schema.String,
host: Schema.String,
desktopBootstrapToken: Schema.String,
tailscaleServeEnabled: Schema.Boolean,
- tailscaleServePort: Schema.Number,
+ tailscaleServePort: PortSchema,
otlpTracesUrl: Schema.optional(Schema.String),
otlpMetricsUrl: Schema.optional(Schema.String),
});You can send follow-ups to the cloud agent here.
|
Bugbot Autofix prepared a fix for the issue found in the latest run.
Or push these changes by commenting: Preview (fbac652b11)diff --git a/apps/desktop/src/desktopBackendManager.ts b/apps/desktop/src/desktopBackendManager.ts
--- a/apps/desktop/src/desktopBackendManager.ts
+++ b/apps/desktop/src/desktopBackendManager.ts
@@ -232,11 +232,11 @@
.exists(config.entryPath)
.pipe(Effect.orElseSucceed(() => false));
+ yield* cancelRestart;
yield* Ref.update(state, (latest) => ({
...latest,
desiredRunning: true,
ready: false,
- restartFiber: Option.none(),
}));
if (!entryExists) {You can send follow-ups to the cloud agent here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 4 total unresolved issues (including 3 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Restart backoff resets on spawn, not readiness
- Moved the
restartAttempt: 0reset from theonStartedcallback to theonReadycallback so exponential backoff is maintained for backends that crash during initialization.
- Moved the
Or push these changes by commenting:
@cursor push 269ea9ef0e
Preview (269ea9ef0e)
diff --git a/apps/desktop/src/desktopBackendManager.ts b/apps/desktop/src/desktopBackendManager.ts
--- a/apps/desktop/src/desktopBackendManager.ts
+++ b/apps/desktop/src/desktopBackendManager.ts
@@ -324,16 +324,13 @@
...run,
pid: Option.some(pid),
}));
- yield* Ref.update(state, (latest) => ({
- ...latest,
- restartAttempt: 0,
- }));
yield* events.onStarted({ pid, config });
}),
onReady: () =>
Effect.gen(function* () {
yield* Ref.update(state, (latest) => ({
...latest,
+ restartAttempt: 0,
ready: Option.match(latest.active, {
onNone: () => latest.ready,
onSome: (run) => (run.id === runId ? true : latest.ready),You can send follow-ups to the cloud agent here.
Co-authored-by: codex <codex@users.noreply.github.com>
01e1673 to
7bdfc8c
Compare
- Remove explicit shutdown paths - Rely on scoped finalizers for backend, updates, and SSH prompts - Co-authored-by: codex <codex@users.noreply.github.com>
| ); | ||
| }); | ||
|
|
||
| const startUpdatePollers: Effect.Effect<void, never, Scope.Scope> = Effect.gen(function* () { |
There was a problem hiding this comment.
🟢 Low updates/DesktopUpdates.ts:408
The polling loop at startUpdatePollers breaks permanently when a defect (runtime exception) occurs during checkForUpdates. The Effect.catchCause is placed outside Effect.forever, so when an uncaught defect propagates from the inner effect, catchCause logs it and returns, terminating the fiber. Since checkForUpdates only uses Effect.catch for typed errors, any unexpected throw from electronUpdater.checkForUpdates or other operations stops update polling until app restart. Consider moving catchCause inside forever so each iteration is isolated, or wrapping checkForUpdates with Effect.catchAllDefect to ensure defects are caught and logged without breaking the loop.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/desktop/src/updates/DesktopUpdates.ts around line 408:
The polling loop at `startUpdatePollers` breaks permanently when a defect (runtime exception) occurs during `checkForUpdates`. The `Effect.catchCause` is placed **outside** `Effect.forever`, so when an uncaught defect propagates from the inner effect, `catchCause` logs it and returns, terminating the fiber. Since `checkForUpdates` only uses `Effect.catch` for typed errors, any unexpected throw from `electronUpdater.checkForUpdates` or other operations stops update polling until app restart. Consider moving `catchCause` inside `forever` so each iteration is isolated, or wrapping `checkForUpdates` with `Effect.catchAllDefect` to ensure defects are caught and logged without breaking the loop.
Evidence trail:
apps/desktop/src/updates/DesktopUpdates.ts lines 408-426 (startUpdatePollers with Effect.forever before Effect.catchCause in pipe chain); lines 303-336 (checkForUpdates using only Effect.catch for typed errors, not catching defects); lines 324-326 (electronUpdater.checkForUpdates piped through Effect.catch only)
- Remove the DesktopRun service and derive run IDs from scoped log annotations - Update backend lifecycle and menu logging to use Effect logging helpers - Adjust tests for backend restarts and menu behavior
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Misleading log emitted when backend start is skipped
- Moved the log statement inside the conditional block and added a distinct "skipped" log for the quitting case, so logs now accurately reflect whether the backend was started or skipped.
Or push these changes by commenting:
@cursor push 5f643f6923
Preview (5f643f6923)
diff --git a/apps/desktop/src/app/DesktopApp.ts b/apps/desktop/src/app/DesktopApp.ts
--- a/apps/desktop/src/app/DesktopApp.ts
+++ b/apps/desktop/src/app/DesktopApp.ts
@@ -178,8 +178,10 @@
if (!(yield* Ref.get(state.quitting))) {
yield* backendManager.start;
+ yield* Effect.logInfo("bootstrap backend start requested");
+ } else {
+ yield* Effect.logInfo("bootstrap backend start skipped (quitting)");
}
- yield* Effect.logInfo("bootstrap backend start requested");
if (environment.isDevelopment) {
yield* desktopWindow.ensureMain;You can send follow-ups to the cloud agent here.
- Switch desktop backend readiness checks to `/.well-known/t3/environment` - Update tests to expect the new probe URL
- Persist backend output and desktop logs in development - Delay dev window creation until backend is ready - Move Electron scheme privilege registration into startup layer
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed:
desktopState.backendReadyis never set to true- Added
Ref.set(desktopState.backendReady, true)in theonReadycallback and removed the manual workaround in the test that externally set it to true.
- Added
Or push these changes by commenting:
@cursor push e5394e09b4
Preview (e5394e09b4)
diff --git a/apps/desktop/src/backend/DesktopBackendManager.test.ts b/apps/desktop/src/backend/DesktopBackendManager.test.ts
--- a/apps/desktop/src/backend/DesktopBackendManager.test.ts
+++ b/apps/desktop/src/backend/DesktopBackendManager.test.ts
@@ -328,7 +328,6 @@
yield* manager.start;
assert.equal(yield* Queue.take(startedPids), 123);
yield* Deferred.await(ready);
- yield* Ref.set(backendReady, true);
assert.isTrue(yield* Ref.get(backendReady));
assert.deepEqual(yield* manager.currentConfig, Option.some(baseConfig));
diff --git a/apps/desktop/src/backend/DesktopBackendManager.ts b/apps/desktop/src/backend/DesktopBackendManager.ts
--- a/apps/desktop/src/backend/DesktopBackendManager.ts
+++ b/apps/desktop/src/backend/DesktopBackendManager.ts
@@ -443,6 +443,7 @@
onSome: (run) => (run.id === runId ? true : latest.ready),
}),
}));
+ yield* Ref.set(desktopState.backendReady, true);
yield* desktopWindow.handleBackendReady.pipe(
Effect.catch((error) =>
Effect.logError("failed to open main window after backend readiness").pipe(You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 413d2f9. Configure here.
- Move trace sink and OTLP helpers into shared observability code - Add desktop trace export and structured backend child logging - Update server observability wiring and tests for new trace records
- Ignore stale ready signals from previous runs - Skip scheduled restarts once desiredRunning is false - Add regression coverage for stop cancelling a pending restart
- wrap startup, lifecycle, backend, IPC, and settings flows with spans - remove the separate desktop-main.log file logger in favor of trace events - keep observability tests aligned with span-based logging
Co-authored-by: codex <codex@users.noreply.github.com>
- Ignore windows that are destroyed before sync runs - Add coverage for appearance sync filtering
- Introduce a shared component logger helper for desktop services - Replace ad hoc log annotations across startup, backend, updates, menu, and window code


Summary
URLhandling for backend endpoints and the new readiness error type.Testing
bun fmtbun lintbun typecheckbun run testforapps/desktop/src/backendReadiness.test.tsandapps/desktop/src/backendStartupReadiness.test.tsNote
High Risk
Large refactor of the Electron desktop main process, backend spawning/readiness, and lifecycle handling; mistakes could prevent the app from starting or shut down cleanly. Also changes runtime configuration/paths and protocol handling, which can impact packaging and local file access.
Overview
Ports the desktop Electron main process to an Effect-based architecture, introducing layered services for app identity/environment/config, lifecycle/shutdown coordination, observability/tracing, and Electron wrappers (app/dialog/menu/protocol).
Reworks desktop backend startup into Effect-managed spawning with fd-based bootstrap payloads, HTTP readiness polling, structured child-process output logging, and automatic restart scheduling/cancellation logic.
Cleans up legacy promise/imperative utilities and tests (e.g.,
backendPort, readiness helpers, settings/persistence/dialog helpers) in favor of new Effect/Vitest test coverage, and updates build hygiene (.gitignore consolidation) plus bumpselectronto41.5.0.Reviewed by Cursor Bugbot for commit 35835a7. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Port desktop app to Effect by replacing imperative Electron code with layered Effect-TS services
NodeRuntime.runMainto execute aDesktopApp.programwith all services provided via layers.ElectronApp,ElectronDialog,ElectronMenu,ElectronProtocol,ElectronSafeStorage,ElectronShell,ElectronTheme,ElectronUpdater, andElectronWindow, each with typed errors and scoped resource management.DesktopEnvironment,DesktopAppSettings,DesktopClientSettings,DesktopSavedEnvironments,DesktopBackendManager,DesktopBackendConfiguration,DesktopServerExposure,DesktopSshEnvironment,DesktopUpdates,DesktopWindow, etc.) as injectable Effect layers.compactTraceAttributes) from the server app to@t3tools/shared/observability, and movesDesktopBackendBootstrapschema to@t3tools/contracts.DesktopIpcandinstallDesktopIpcHandlers, covering settings, SSH, server exposure, updates, and window operations; the preload bridge now sends structured object payloads instead of positional arguments.packages/contracts,packages/ssh,packages/shared,packages/tailscale, and related scripts to namespaced form, enforced by@effect/language-serviceplugin added to multipletsconfig.jsonfiles.main.tsis a wholesale rewrite; any existing behavior not covered by the new layers (imperative window creation, old IPC handler signatures, title bar overlay config) is removed or changed.Macroscope summarized 35835a7.