@@ -25,6 +25,7 @@ import * as Context from "effect/Context";
2525import * as Effect from "effect/Effect" ;
2626import * as Layer from "effect/Layer" ;
2727import * as Option from "effect/Option" ;
28+ import * as Ref from "effect/Ref" ;
2829import * as Semaphore from "effect/Semaphore" ;
2930
3031import * as NetService from "@t3tools/shared/Net" ;
@@ -49,6 +50,11 @@ export interface DesktopWslBackendShape {
4950 // Idempotent. Never fails (errors are logged); callers can chain it
5051 // after persisting settings without an error-handling dance.
5152 readonly reconcile : Effect . Effect < void > ;
53+ // Reason the dual-mode WSL secondary last failed preflight (no node, wrong
54+ // version, missing build tools), or None. Read by the getWslState IPC so
55+ // Connections settings can show it inline. None in wsl-only mode (that path
56+ // surfaces via a dialog + Windows fallback).
57+ readonly lastPreflightError : Effect . Effect < Option . Option < string > > ;
5258}
5359
5460export class DesktopWslBackend extends Context . Service < DesktopWslBackend , DesktopWslBackendShape > ( ) (
@@ -102,6 +108,13 @@ export const layer = Layer.effect(
102108 // with different distros, leaving the loser stranded.
103109 const reconcileMutex = yield * Semaphore . make ( 1 ) ;
104110
111+ // Last fatal preflight failure from the dual-mode WSL *secondary*, surfaced
112+ // inline in Connections settings. The primary's failure is handled by the
113+ // pool (dialog + Windows fallback) instead; here the app stays usable on
114+ // Windows, so we record the reason rather than interrupting. Cleared on any
115+ // reconcile state change so it reflects the current attempt.
116+ const preflightErrorRef = yield * Ref . make ( Option . none < string > ( ) ) ;
117+
105118 const findExistingWslInstance = pool . list . pipe (
106119 Effect . map ( ( instances ) => instances . find ( ( instance ) => isWslInstanceId ( instance . id ) ) ) ,
107120 Effect . map ( Option . fromNullishOr ) ,
@@ -150,6 +163,10 @@ export const layer = Layer.effect(
150163 id : targetId ,
151164 label : Effect . succeed ( buildLabel ( input . distro ) ) ,
152165 configResolve : configuration . resolveWsl ( { port : allocatedPort , distro : input . distro } ) ,
166+ // Dual-mode secondary: record a fatal preflight failure so Connections
167+ // settings can show why the WSL backend never appeared. No dialog or
168+ // fallback — Windows is the primary and keeps working.
169+ onPreflightFailed : ( reason ) => Ref . set ( preflightErrorRef , Option . some ( reason ) ) ,
153170 } )
154171 . pipe (
155172 Effect . map ( ( registered ) => Option . some ( registered ) ) ,
@@ -195,6 +212,11 @@ export const layer = Layer.effect(
195212 return ;
196213 }
197214
215+ // A real state change is happening (start, stop, or distro swap). Clear
216+ // any stale secondary preflight error so it reflects this fresh attempt;
217+ // onPreflightFailed re-sets it only if the new secondary exhausts retries.
218+ yield * Ref . set ( preflightErrorRef , Option . none ( ) ) ;
219+
198220 if ( Option . isSome ( existingId ) ) {
199221 yield * logWslBackendInfo ( "tearing down WSL backend" , { id : existingId . value } ) ;
200222 yield * stopExisting ( existingId . value ) ;
@@ -227,6 +249,9 @@ export const layer = Layer.effect(
227249 Effect . withSpan ( "desktop.wslBackend.reconcile" ) ,
228250 ) ;
229251
230- return DesktopWslBackend . of ( { reconcile } ) ;
252+ return DesktopWslBackend . of ( {
253+ reconcile,
254+ lastPreflightError : Ref . get ( preflightErrorRef ) ,
255+ } ) ;
231256 } ) ,
232257) ;
0 commit comments