Skip to content

[codex] Migrate desktop shell and SSH Effect services - #3194

Merged
juliusmarminge merged 3 commits into
mainfrom
codex/effect-service-desktop-ssh-shell
Jun 20, 2026
Merged

[codex] Migrate desktop shell and SSH Effect services#3194
juliusmarminge merged 3 commits into
mainfrom
codex/effect-service-desktop-ssh-shell

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary

  • standardize the root-co-located DesktopShellEnvironment, DesktopSshEnvironment, and DesktopSshPasswordPrompts service modules
  • inline each Context.Service contract and use Service["Service"] for implementation and dependency shapes
  • export each concrete, effectful make and canonical layer; Layer.effect remains appropriate here because the constructors have real dependencies
  • use namespace imports and qualified consumers for dedicated service modules while retaining named imports for contracts, errors, and helper package APIs
  • migrate SSH prompt failures to Schema.TaggedErrorClass with contextual attributes and messages derived from those attributes
  • expose a Schema.Union for cancellation predicates while keeping Effect error channels as class unions
  • preserve the distinct window-unavailable failure when the Electron window is destroyed during prompt dispatch

Error audit

No error in this domain uses a reason, kind, or operation switch to hide materially different failures, so no class split is required. DesktopSshPromptCancelledError.reason remains internal diagnostic context for one cancellation failure mode. Secure-randomness failure now retains its destination and underlying cause.

Scope and impact

The three service modules were already co-located at their domain roots, so no compatibility shims or path migrations are needed. Runtime service behavior and layer options remain intact, with one race correction: window loss during prompt dispatch remains a window-unavailable error instead of being reclassified as a generic send failure. SSH prompt errors now carry richer structured context and more specific messages.

Existing desktop shell/SSH suites cover these paths; no test files or refactor-only test declarations are added. No orchestration modules, MCP modules, or integration harnesses are changed.

Validation

  • focused desktop shell/SSH tests — 3 files, 9 tests passed
  • vp check — passed with 0 errors; 20 pre-existing warnings remain
  • vp run typecheck — all 15 workspace tasks passed
  • git diff --check origin/main — passed
  • canonical audit — 3/3 exported concrete make values, 3/3 exported layer values, 0 standalone service shapes, namespace-qualified service modules, and named non-service package APIs
  • error audit — 7 schema-backed error classes, no switch-based hidden unions, and one exported cancellation schema union

Note

Medium Risk
Touches SSH authentication error mapping and cancellation semantics; user-visible messages are mostly preserved but internal classification and cancellation breadth changed.

Overview
Refactors desktop shell and SSH Effect services to match the co-located service pattern: contracts are inlined on Context.Service, exported make factories replace inline Layer.effect bodies, and imports move to namespace modules (ChildProcessSpawner, SshAuth, SshTunnel, etc.).

SSH password prompts are reworked around Schema.TaggedErrorClass with richer structured causes (request-id failure, presentation, window closed, service stopped). cancelPending is removed from the public service API; shutdown now fails pending prompts via a finalizer with DesktopSshPromptServiceStoppedError. toSshPasswordPromptError centralizes mapping to SshPasswordPromptError, including treating presentation failures like window unavailable for the user-facing message while keeping distinct diagnostic messages on the underlying errors. Cancellation detection broadens via a Schema.Union (cancelled, timed out, window closed, service stopped). Prompt dispatch reclassifies destroyed-window races as window-unavailable instead of generic send failures.

Adds a test that presentation diagnostics stay distinct from the wrapped SSH auth message.

Reviewed by Cursor Bugbot for commit 1a291d8. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Migrate desktop shell and SSH Effect services to namespaced imports with structured error types

  • Refactors DesktopShellEnvironment.ts and DesktopSshEnvironment.ts to use module-namespaced imports (e.g. * as SshAuth, * as SshTunnel) and extracts make factories from inline Layer.effect calls.
  • Reworks SSH password prompt errors in DesktopSshPasswordPrompts.ts to use Schema.TaggedErrorClass, adding structured types for request ID failures, presentation errors, window-closed, and service-stopped conditions.
  • Removes cancelPending from the DesktopSshPasswordPrompts service shape; cancellation is now handled internally via a finalizer that emits DesktopSshPromptServiceStoppedError.
  • Adds toSshPasswordPromptError in DesktopSshEnvironment to map all prompt error variants to SshPasswordPromptError with standardized messages; both window-unavailable and presentation errors map to the same user-facing message.
  • Behavioral Change: DesktopSshPasswordPromptCancellation now includes window-closed and service-stopped alongside cancelled and timed-out, broadening the cancellation predicate.

Macroscope summarized 1a291d8.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3b7bf7a8-d5b3-4623-982c-c44d73e9f356

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/effect-service-desktop-ssh-shell

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jun 20, 2026
@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

🚀 Expo continuous deployment is ready!

  • Project → t3-code
  • Platforms → android, ios
  • Scheme → t3code-preview
  🤖 Android 🍎 iOS
Fingerprint fe5a51f2e189da69dfc4c2cd458e6cfb5fdff2ea ae3bd597809dfd7771d0898f735d172973d4c1c8
Build Details Build Permalink
DetailsDistribution: INTERNAL
Build profile: preview:dev
Runtime version: fe5a51f2e189da69dfc4c2cd458e6cfb5fdff2ea
App version: 0.1.0
Git commit: d0f282101e54a1f4777321725f9e7e659acd39f5
Build Permalink
DetailsDistribution: INTERNAL
Build profile: preview:dev
Runtime version: ae3bd597809dfd7771d0898f735d172973d4c1c8
App version: 0.1.0
Git commit: 318593fd88314a388ad3503e67042249907a3418
Update Details Update Permalink
DetailsBranch: pr-3194
Runtime version: fe5a51f2e189da69dfc4c2cd458e6cfb5fdff2ea
Git commit: 7f67f1cb037b4e650d8c14c7f13f03afcf34c36c
Update Permalink
DetailsBranch: pr-3194
Runtime version: ae3bd597809dfd7771d0898f735d172973d4c1c8
Git commit: 7f67f1cb037b4e650d8c14c7f13f03afcf34c36c
Update QR

@juliusmarminge
juliusmarminge marked this pull request as ready for review June 20, 2026 02:06
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Jun 20, 2026
@macroscopeapp

macroscopeapp Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved

Mechanical migration of Effect services to newer patterns - updates import styles, converts error classes to Schema.TaggedErrorClass, and moves interface definitions inline. No significant runtime behavior changes; error messages remain functionally equivalent.

No code changes detected at 1a291d8. Prior analysis still applies.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge
juliusmarminge force-pushed the codex/effect-service-desktop-ssh-shell branch from 17505cc to 36276e8 Compare June 20, 2026 02:33
@macroscopeapp
macroscopeapp Bot dismissed their stale review June 20, 2026 02:33

Dismissing prior approval to re-evaluate 36276e8

macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Jun 20, 2026
@juliusmarminge
juliusmarminge force-pushed the codex/effect-service-desktop-ssh-shell branch from 36276e8 to 323bc31 Compare June 20, 2026 03:42
@macroscopeapp
macroscopeapp Bot dismissed their stale review June 20, 2026 03:42

Dismissing prior approval to re-evaluate 323bc31

@juliusmarminge
juliusmarminge force-pushed the codex/effect-service-desktop-ssh-shell branch from 323bc31 to 5291b43 Compare June 20, 2026 03:44

@cursor cursor Bot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Send error hides window loss
    • The catch handler in Effect.try now detects window-unavailable errors and returns DesktopSshPromptWindowUnavailableError instead of wrapping them in DesktopSshPromptSendError, so users see the correct window-unavailable message.

Create PR

Or push these changes by commenting:

@cursor push f5a583965d
Preview (f5a583965d)
diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.ts b/apps/desktop/src/shell/DesktopShellEnvironment.ts
--- a/apps/desktop/src/shell/DesktopShellEnvironment.ts
+++ b/apps/desktop/src/shell/DesktopShellEnvironment.ts
@@ -3,7 +3,8 @@
 import * as Effect from "effect/Effect";
 import * as Layer from "effect/Layer";
 import * as Option from "effect/Option";
-import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
+import * as ChildProcess from "effect/unstable/process/ChildProcess";
+import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
 
 import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
 
@@ -19,13 +20,11 @@
   readonly loadProfile: boolean;
 }
 
-export interface DesktopShellEnvironmentShape {
-  readonly installIntoProcess: Effect.Effect<void, never>;
-}
-
 export class DesktopShellEnvironment extends Context.Service<
   DesktopShellEnvironment,
-  DesktopShellEnvironmentShape
+  {
+    readonly installIntoProcess: Effect.Effect<void>;
+  }
 >()("@t3tools/desktop/shell/DesktopShellEnvironment") {}
 
 const LOGIN_SHELL_ENV_NAMES = [
@@ -336,20 +335,20 @@
   return Effect.void;
 };
 
-export const layer = Layer.effect(
-  DesktopShellEnvironment,
-  Effect.gen(function* () {
-    const environment = yield* DesktopEnvironment.DesktopEnvironment;
-    const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
-    return DesktopShellEnvironment.of({
-      installIntoProcess: installShellEnvironment({
-        env: process.env,
-        platform: environment.platform,
-        userShell: Option.none(),
-      }).pipe(
-        Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
-        Effect.withSpan("desktop.shellEnvironment.installIntoProcess"),
-      ),
-    });
-  }),
-);
+export const make = Effect.gen(function* () {
+  const environment = yield* DesktopEnvironment.DesktopEnvironment;
+  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
+  const installIntoProcess: DesktopShellEnvironment["Service"]["installIntoProcess"] =
+    installShellEnvironment({
+      env: process.env,
+      platform: environment.platform,
+      userShell: Option.none(),
+    }).pipe(
+      Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
+      Effect.withSpan("desktop.shellEnvironment.installIntoProcess"),
+    );
+
+  return DesktopShellEnvironment.of({ installIntoProcess });
+});
+
+export const layer = Layer.effect(DesktopShellEnvironment, make);

diff --git a/apps/desktop/src/ssh/DesktopSshEnvironment.ts b/apps/desktop/src/ssh/DesktopSshEnvironment.ts
--- a/apps/desktop/src/ssh/DesktopSshEnvironment.ts
+++ b/apps/desktop/src/ssh/DesktopSshEnvironment.ts
@@ -4,11 +4,7 @@
   DesktopSshEnvironmentTarget,
 } from "@t3tools/contracts";
 import * as NetService from "@t3tools/shared/Net";
-import {
-  SshPasswordPrompt,
-  type SshPasswordPromptShape,
-  type SshPasswordRequest,
-} from "@t3tools/ssh/auth";
+import { SshPasswordPrompt, type SshPasswordRequest } from "@t3tools/ssh/auth";
 import { discoverSshHosts } from "@t3tools/ssh/config";
 import {
   SshCommandError,
@@ -25,8 +21,8 @@
 import * as FileSystem from "effect/FileSystem";
 import * as Layer from "effect/Layer";
 import * as Path from "effect/Path";
-import { HttpClient } from "effect/unstable/http";
-import { ChildProcessSpawner } from "effect/unstable/process";
+import * as HttpClient from "effect/unstable/http/HttpClient";
+import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
 
 import * as DesktopSshPasswordPrompts from "./DesktopSshPasswordPrompts.ts";
 
@@ -52,22 +48,20 @@
   | DesktopSshEnvironmentDiscoverError
   | DesktopSshEnvironmentOperationError;
 
-export interface DesktopSshEnvironmentShape {
-  readonly discoverHosts: (input?: {
-    readonly homeDir?: string;
-  }) => Effect.Effect<readonly DesktopDiscoveredSshHost[], DesktopSshEnvironmentDiscoverError>;
-  readonly ensureEnvironment: (
-    target: DesktopSshEnvironmentTarget,
-    options?: { readonly issuePairingToken?: boolean },
-  ) => Effect.Effect<DesktopSshEnvironmentBootstrap, DesktopSshEnvironmentOperationError>;
-  readonly disconnectEnvironment: (
-    target: DesktopSshEnvironmentTarget,
-  ) => Effect.Effect<void, DesktopSshEnvironmentOperationError>;
-}
-
 export class DesktopSshEnvironment extends Context.Service<
   DesktopSshEnvironment,
-  DesktopSshEnvironmentShape
+  {
+    readonly discoverHosts: (input?: {
+      readonly homeDir?: string;
+    }) => Effect.Effect<readonly DesktopDiscoveredSshHost[], DesktopSshEnvironmentDiscoverError>;
+    readonly ensureEnvironment: (
+      target: DesktopSshEnvironmentTarget,
+      options?: { readonly issuePairingToken?: boolean },
+    ) => Effect.Effect<DesktopSshEnvironmentBootstrap, DesktopSshEnvironmentOperationError>;
+    readonly disconnectEnvironment: (
+      target: DesktopSshEnvironmentTarget,
+    ) => Effect.Effect<void, DesktopSshEnvironmentOperationError>;
+  }
 >()("@t3tools/desktop/ssh/DesktopSshEnvironment") {}
 
 export interface DesktopSshEnvironmentLayerOptions {
@@ -89,8 +83,8 @@
 }
 
 const makePasswordPrompt = (
-  prompts: DesktopSshPasswordPrompts.DesktopSshPasswordPromptsShape,
-): SshPasswordPromptShape => ({
+  prompts: DesktopSshPasswordPrompts.DesktopSshPasswordPrompts["Service"],
+): SshPasswordPrompt["Service"] => ({
   isAvailable: true,
   request: (request: SshPasswordRequest) =>
     prompts.request(request).pipe(
@@ -104,7 +98,7 @@
     ),
 });
 
-const make = Effect.gen(function* () {
+export const make = Effect.gen(function* () {
   const manager = yield* SshEnvironmentManager;
   const prompts = yield* DesktopSshPasswordPrompts.DesktopSshPasswordPrompts;
   const runtimeContext = yield* Effect.context<DesktopSshEnvironmentRuntimeServices>();

diff --git a/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts b/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts
--- a/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts
+++ b/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts
@@ -3,7 +3,6 @@
 import type { SshPasswordRequest } from "@t3tools/ssh/auth";
 import * as Context from "effect/Context";
 import * as Crypto from "effect/Crypto";
-import * as Data from "effect/Data";
 import * as DateTime from "effect/DateTime";
 import * as Deferred from "effect/Deferred";
 import * as Duration from "effect/Duration";
@@ -11,84 +10,99 @@
 import * as Layer from "effect/Layer";
 import * as Option from "effect/Option";
 import * as Ref from "effect/Ref";
+import * as Schema from "effect/Schema";
 
 import * as IpcChannels from "../ipc/channels.ts";
 import * as ElectronWindow from "../electron/ElectronWindow.ts";
 
 const DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS = 3 * 60 * 1000;
-const WINDOW_UNAVAILABLE_MESSAGE = "T3 Code window is not available for SSH authentication.";
 
 type DesktopSshPasswordPromptResolutionInput =
   typeof DesktopSshPasswordPromptResolutionInputSchema.Type;
 
-export class DesktopSshPromptUnavailableError extends Data.TaggedError(
+const windowUnavailableMessage = (destination: string): string =>
+  `T3 Code window is not available for SSH authentication to ${destination}.`;
+
+export class DesktopSshPromptUnavailableError extends Schema.TaggedErrorClass<DesktopSshPromptUnavailableError>()(
   "DesktopSshPromptUnavailableError",
-)<{
-  readonly reason: string;
-}> {
-  override get message() {
-    return this.reason;
+  {
+    destination: Schema.String,
+    cause: Schema.Defect(),
+  },
+) {
+  override get message(): string {
+    return `Secure randomness is unavailable for SSH authentication to ${this.destination}.`;
   }
 }
 
-export class DesktopSshPromptWindowUnavailableError extends Data.TaggedError(
+export class DesktopSshPromptWindowUnavailableError extends Schema.TaggedErrorClass<DesktopSshPromptWindowUnavailableError>()(
   "DesktopSshPromptWindowUnavailableError",
-)<{
-  readonly destination: string;
-}> {
-  override get message() {
-    return WINDOW_UNAVAILABLE_MESSAGE;
+  {
+    destination: Schema.String,
+  },
+) {
+  override get message(): string {
+    return windowUnavailableMessage(this.destination);
   }
 }
 
-export class DesktopSshPromptSendError extends Data.TaggedError("DesktopSshPromptSendError")<{
-  readonly requestId: string;
-  readonly destination: string;
-  readonly cause: unknown;
-}> {
-  override get message() {
-    return WINDOW_UNAVAILABLE_MESSAGE;
+export class DesktopSshPromptSendError extends Schema.TaggedErrorClass<DesktopSshPromptSendError>()(
+  "DesktopSshPromptSendError",
+  {
+    requestId: Schema.String,
+    destination: Schema.String,
+    cause: Schema.Defect(),
+  },
+) {
+  override get message(): string {
+    return `Failed to send SSH password prompt ${this.requestId} for ${this.destination}.`;
   }
 }
 
-export class DesktopSshPromptTimedOutError extends Data.TaggedError(
+export class DesktopSshPromptTimedOutError extends Schema.TaggedErrorClass<DesktopSshPromptTimedOutError>()(
   "DesktopSshPromptTimedOutError",
-)<{
-  readonly requestId: string;
-  readonly destination: string;
-}> {
-  override get message() {
+  {
+    requestId: Schema.String,
+    destination: Schema.String,
+  },
+) {
+  override get message(): string {
     return `SSH authentication timed out for ${this.destination}.`;
   }
 }
 
-export class DesktopSshPromptCancelledError extends Data.TaggedError(
+export class DesktopSshPromptCancelledError extends Schema.TaggedErrorClass<DesktopSshPromptCancelledError>()(
   "DesktopSshPromptCancelledError",
-)<{
-  readonly requestId: string;
-  readonly destination: string;
-  readonly reason: string;
-}> {
-  override get message() {
+  {
+    requestId: Schema.String,
+    destination: Schema.String,
+    reason: Schema.String,
+  },
+) {
+  override get message(): string {
     return this.reason;
   }
 }
 
-export class DesktopSshPromptInvalidRequestIdError extends Data.TaggedError(
+export class DesktopSshPromptInvalidRequestIdError extends Schema.TaggedErrorClass<DesktopSshPromptInvalidRequestIdError>()(
   "DesktopSshPromptInvalidRequestIdError",
-)<{
-  readonly requestId: string;
-}> {
-  override get message() {
-    return "Invalid SSH password prompt id.";
+  {
+    requestId: Schema.String,
+  },
+) {
+  override get message(): string {
+    return `Invalid SSH password prompt id: ${this.requestId}.`;
   }
 }
 
-export class DesktopSshPromptExpiredError extends Data.TaggedError("DesktopSshPromptExpiredError")<{
-  readonly requestId: string;
-}> {
-  override get message() {
-    return "SSH password prompt expired. Try connecting again.";
+export class DesktopSshPromptExpiredError extends Schema.TaggedErrorClass<DesktopSshPromptExpiredError>()(
+  "DesktopSshPromptExpiredError",
+  {
+    requestId: Schema.String,
+  },
+) {
+  override get message(): string {
+    return `SSH password prompt ${this.requestId} expired. Try connecting again.`;
   }
 }
 
@@ -107,28 +121,31 @@
   | DesktopSshPasswordPromptRequestError
   | DesktopSshPasswordPromptResolveError;
 
+export const DesktopSshPasswordPromptCancellation = Schema.Union([
+  DesktopSshPromptCancelledError,
+  DesktopSshPromptTimedOutError,
+]);
+export type DesktopSshPasswordPromptCancellation = typeof DesktopSshPasswordPromptCancellation.Type;
+
+const isDesktopSshPasswordPromptCancellationValue = Schema.is(DesktopSshPasswordPromptCancellation);
+
 export function isDesktopSshPasswordPromptCancellation(
   error: unknown,
-): error is DesktopSshPromptCancelledError | DesktopSshPromptTimedOutError {
-  return (
-    error instanceof DesktopSshPromptCancelledError ||
-    error instanceof DesktopSshPromptTimedOutError
-  );
+): error is DesktopSshPasswordPromptCancellation {
+  return isDesktopSshPasswordPromptCancellationValue(error);
 }
 
-export interface DesktopSshPasswordPromptsShape {
-  readonly request: (
-    request: SshPasswordRequest,
-  ) => Effect.Effect<string, DesktopSshPasswordPromptRequestError>;
-  readonly resolve: (
-    input: DesktopSshPasswordPromptResolutionInput,
-  ) => Effect.Effect<void, DesktopSshPasswordPromptResolveError>;
-  readonly cancelPending: (reason: string) => Effect.Effect<void>;
-}
-
 export class DesktopSshPasswordPrompts extends Context.Service<
   DesktopSshPasswordPrompts,
-  DesktopSshPasswordPromptsShape
+  {
+    readonly request: (
+      request: SshPasswordRequest,
+    ) => Effect.Effect<string, DesktopSshPasswordPromptRequestError>;
+    readonly resolve: (
+      input: DesktopSshPasswordPromptResolutionInput,
+    ) => Effect.Effect<void, DesktopSshPasswordPromptResolveError>;
+    readonly cancelPending: (reason: string) => Effect.Effect<void>;
+  }
 >()("@t3tools/desktop/ssh/DesktopSshPasswordPrompts") {}
 
 interface PendingSshPasswordPrompt {
@@ -137,7 +154,7 @@
   readonly deferred: Deferred.Deferred<string, DesktopSshPasswordPromptRequestError>;
 }
 
-interface LayerOptions {
+export interface DesktopSshPasswordPromptsOptions {
   readonly passwordPromptTimeoutMs?: number;
 }
 
@@ -161,14 +178,16 @@
   error: DesktopSshPasswordPromptRequestError,
 ) => Deferred.fail(pending.deferred, error).pipe(Effect.asVoid);
 
-const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (options: LayerOptions = {}) {
+export const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (
+  options: DesktopSshPasswordPromptsOptions = {},
+) {
   const electronWindow = yield* ElectronWindow.ElectronWindow;
   const crypto = yield* Crypto.Crypto;
   const pendingRef = yield* Ref.make(new Map<string, PendingSshPasswordPrompt>());
   const passwordPromptTimeoutMs =
     options.passwordPromptTimeoutMs ?? DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS;
 
-  const cancelPending = (reason: string): Effect.Effect<void> =>
+  const cancelPending: DesktopSshPasswordPrompts["Service"]["cancelPending"] = (reason) =>
     Ref.getAndSet(pendingRef, new Map()).pipe(
       Effect.flatMap((pending) =>
         Effect.forEach(
@@ -192,9 +211,9 @@
     cancelPending("SSH password prompt service stopped.").pipe(Effect.ignore),
   );
 
-  const resolve = Effect.fn("desktop.sshPasswordPrompts.resolve")(function* (
-    input: DesktopSshPasswordPromptResolutionInput,
-  ): Effect.fn.Return<void, DesktopSshPasswordPromptResolveError> {
+  const resolve: DesktopSshPasswordPrompts["Service"]["resolve"] = Effect.fn(
+    "desktop.sshPasswordPrompts.resolve",
+  )(function* (input) {
     const requestId = input.requestId.trim();
     if (requestId.length === 0) {
       return yield* new DesktopSshPromptInvalidRequestIdError({ requestId: input.requestId });
@@ -221,9 +240,9 @@
     yield* Deferred.succeed(entry.deferred, input.password).pipe(Effect.asVoid);
   });
 
-  const request = Effect.fn("desktop.sshPasswordPrompts.request")(function* (
-    input: SshPasswordRequest,
-  ): Effect.fn.Return<string, DesktopSshPasswordPromptRequestError> {
+  const request: DesktopSshPasswordPrompts["Service"]["request"] = Effect.fn(
+    "desktop.sshPasswordPrompts.request",
+  )(function* (input) {
     const window = yield* electronWindow.main;
     if (Option.isNone(window) || window.value.isDestroyed()) {
       return yield* new DesktopSshPromptWindowUnavailableError({
@@ -233,7 +252,11 @@
 
     const requestId = yield* crypto.randomUUIDv4.pipe(
       Effect.mapError(
-        () => new DesktopSshPromptUnavailableError({ reason: "Secure randomness is unavailable." }),
+        (cause) =>
+          new DesktopSshPromptUnavailableError({
+            destination: input.destination,
+            cause,
+          }),
       ),
     );
     const now = yield* DateTime.now;
@@ -302,27 +325,36 @@
     return yield* Effect.try({
       try: () => {
         if (window.value.isDestroyed()) {
-          throw new Error(WINDOW_UNAVAILABLE_MESSAGE);
+          throw new Error(windowUnavailableMessage(input.destination));
         }
         window.value.once("closed", cancelOnWindowClosed);
         window.value.webContents.send(IpcChannels.SSH_PASSWORD_PROMPT_CHANNEL, promptRequest);
         if (window.value.isDestroyed()) {
-          throw new Error(WINDOW_UNAVAILABLE_MESSAGE);
+          throw new Error(windowUnavailableMessage(input.destination));
         }
         if (window.value.isMinimized()) {
           window.value.restore();
         }
         if (window.value.isDestroyed()) {
-          throw new Error(WINDOW_UNAVAILABLE_MESSAGE);
+          throw new Error(windowUnavailableMessage(input.destination));
         }
         window.value.focus();
       },
-      catch: (cause) =>
-        new DesktopSshPromptSendError({
+      catch: (cause) => {
+        if (
+          cause instanceof Error &&
+          cause.message === windowUnavailableMessage(input.destination)
+        ) {
+          return new DesktopSshPromptWindowUnavailableError({
+            destination: input.destination,
+          });
+        }
+        return new DesktopSshPromptSendError({
           requestId,
           destination: input.destination,
           cause,
-        }),
+        });
+      },
     }).pipe(Effect.andThen(waitForPassword), Effect.ensuring(cleanup));
   });
 
@@ -333,5 +365,5 @@
   });
 });
 
-export const layer = (options: LayerOptions = {}) =>
+export const layer = (options: DesktopSshPasswordPromptsOptions = {}) =>
   Layer.effect(DesktopSshPasswordPrompts, make(options));

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 5291b43. Configure here.

Comment thread apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Jun 20, 2026
@juliusmarminge
juliusmarminge force-pushed the codex/effect-service-desktop-ssh-shell branch from 5291b43 to ff9ec3c Compare June 20, 2026 03:51
@macroscopeapp
macroscopeapp Bot dismissed their stale review June 20, 2026 03:51

Dismissing prior approval to re-evaluate ff9ec3c

macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Jun 20, 2026
@juliusmarminge
juliusmarminge force-pushed the codex/effect-service-desktop-ssh-shell branch from ff9ec3c to 552074d Compare June 20, 2026 04:21
@macroscopeapp
macroscopeapp Bot dismissed their stale review June 20, 2026 04:22

Dismissing prior approval to re-evaluate 552074d

macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Jun 20, 2026
@juliusmarminge
juliusmarminge force-pushed the codex/effect-service-desktop-ssh-shell branch from 552074d to 16db048 Compare June 20, 2026 04:27
@macroscopeapp
macroscopeapp Bot dismissed their stale review June 20, 2026 04:27

Dismissing prior approval to re-evaluate 16db048

@juliusmarminge
juliusmarminge force-pushed the codex/effect-service-desktop-ssh-shell branch from 16db048 to 5e42b39 Compare June 20, 2026 04:32
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Jun 20, 2026
@juliusmarminge
juliusmarminge force-pushed the codex/effect-service-desktop-ssh-shell branch 3 times, most recently from d8ddfb2 to b0c4047 Compare June 20, 2026 06:01
@juliusmarminge
juliusmarminge marked this pull request as draft June 20, 2026 06:08
@juliusmarminge
juliusmarminge marked this pull request as ready for review June 20, 2026 06:08
@juliusmarminge
juliusmarminge force-pushed the codex/effect-service-desktop-ssh-shell branch 4 times, most recently from c3c28f8 to 83f8f84 Compare June 20, 2026 06:54
@macroscopeapp
macroscopeapp Bot dismissed their stale review June 20, 2026 06:54

Dismissing prior approval to re-evaluate 83f8f84

macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Jun 20, 2026
@juliusmarminge
juliusmarminge force-pushed the codex/effect-service-desktop-ssh-shell branch 2 times, most recently from fbcbcb0 to 9911fa8 Compare June 20, 2026 07:05
@macroscopeapp
macroscopeapp Bot dismissed their stale review June 20, 2026 07:05

Dismissing prior approval to re-evaluate 9911fa8

Comment thread apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts Outdated
@juliusmarminge
juliusmarminge force-pushed the codex/effect-service-desktop-ssh-shell branch from 9911fa8 to 9697d14 Compare June 20, 2026 07:12
juliusmarminge and others added 2 commits June 20, 2026 00:18
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge
juliusmarminge force-pushed the codex/effect-service-desktop-ssh-shell branch from 9697d14 to a6dec0e Compare June 20, 2026 07:21
Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge
juliusmarminge force-pushed the codex/effect-service-desktop-ssh-shell branch from 24a3874 to 1a291d8 Compare June 20, 2026 07:32
@juliusmarminge
juliusmarminge merged commit 7eda6ba into main Jun 20, 2026
16 checks passed
@juliusmarminge
juliusmarminge deleted the codex/effect-service-desktop-ssh-shell branch June 20, 2026 07:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant