11import { PlusIcon , QrCodeIcon } from "lucide-react" ;
2- import { memo , useCallback , useEffect , useMemo , useState } from "react" ;
2+ import { memo , useCallback , useEffect , useMemo , useRef , useState } from "react" ;
3+ import { useLocation , useNavigate } from "@tanstack/react-router" ;
34import {
45 type AuthClientSession ,
56 type AuthPairingLink ,
@@ -41,6 +42,7 @@ import {
4142} from "../ui/alert-dialog" ;
4243import { Popover , PopoverPopup , PopoverTrigger } from "../ui/popover" ;
4344import { QRCodeSvg } from "../ui/qr-code" ;
45+ import { Select , SelectItem , SelectPopup , SelectTrigger , SelectValue } from "../ui/select" ;
4446import { Spinner } from "../ui/spinner" ;
4547import { Switch } from "../ui/switch" ;
4648import { stackedThreadToast , toastManager } from "../ui/toast" ;
@@ -51,6 +53,9 @@ import { setPairingTokenOnUrl } from "../../pairingUrl";
5153import {
5254 createServerPairingCredential ,
5355 fetchSessionState ,
56+ reauthenticatePrimaryEnvironment ,
57+ refreshPrimaryEnvironmentDescriptor ,
58+ readPrimaryEnvironmentDescriptor ,
5459 revokeOtherServerClientSessions ,
5560 revokeServerClientSession ,
5661 revokeServerPairingLink ,
@@ -69,6 +74,7 @@ import {
6974 reconnectSavedEnvironment ,
7075 removeSavedEnvironment ,
7176} from "~/environments/runtime" ;
77+ import { suppressWsConnectionLifecycle } from "~/rpc/wsConnectionState" ;
7278
7379const accessTimestampFormatter = new Intl . DateTimeFormat ( undefined , {
7480 dateStyle : "medium" ,
@@ -758,6 +764,8 @@ function SavedBackendListRow({
758764}
759765
760766export function ConnectionsSettings ( ) {
767+ const navigate = useNavigate ( ) ;
768+ const pathname = useLocation ( { select : ( location ) => location . pathname } ) ;
761769 const desktopBridge = window . desktopBridge ;
762770 const [ currentSessionRole , setCurrentSessionRole ] = useState < "owner" | "client" | null > (
763771 desktopBridge ? "owner" : null ,
@@ -781,6 +789,9 @@ export function ConnectionsSettings() {
781789 const [ desktopWslDistros , setDesktopWslDistros ] = useState < DesktopWslDistro [ ] > ( [ ] ) ;
782790 const [ desktopWslError , setDesktopWslError ] = useState < string | null > ( null ) ;
783791 const [ isUpdatingDesktopWsl , setIsUpdatingDesktopWsl ] = useState ( false ) ;
792+ const [ desktopWslChangeStage , setDesktopWslChangeStage ] = useState <
793+ "restarting-backend" | "reauthenticating" | null
794+ > ( null ) ;
784795 const [ desktopPairingLinks , setDesktopPairingLinks ] = useState <
785796 ReadonlyArray < ServerPairingLinkRecord >
786797 > ( [ ] ) ;
@@ -816,12 +827,49 @@ export function ConnectionsSettings() {
816827 const [ pendingDesktopServerExposureMode , setPendingDesktopServerExposureMode ] = useState <
817828 DesktopServerExposureState [ "mode" ] | null
818829 > ( null ) ;
830+ const [ pendingDesktopWslConfig , setPendingDesktopWslConfig ] = useState < DesktopWslConfig | null > (
831+ null ,
832+ ) ;
833+ // When WSL is off, the dropdown stages a distro choice locally (no backend hit) until
834+ // the user flips the toggle on. When WSL is on, dropdown changes flow through the
835+ // confirmation dialog like before.
836+ const [ stagedDesktopWslDistro , setStagedDesktopWslDistro ] = useState < string | null > ( null ) ;
837+ const stagedDesktopWslDistroInitializedRef = useRef ( false ) ;
819838 const canManageLocalBackend = currentSessionRole === "owner" ;
820839 const isLocalBackendNetworkAccessible = desktopBridge
821840 ? desktopServerExposureState ?. mode === "network-accessible"
822841 : currentAuthPolicy === "remote-reachable" ;
823- const defaultDesktopWslDistro = desktopWslDistros . find ( ( distro ) => distro . isDefault ) ?. name ?? null ;
824- const selectedDesktopWslDistro = desktopWslConfig ?. distro ?? defaultDesktopWslDistro ;
842+ const defaultDesktopWslDistro =
843+ desktopWslDistros . find ( ( distro ) => distro . isDefault ) ?. name ?? null ;
844+ const effectiveDesktopWslDistro = desktopWslConfig ?. enabled
845+ ? ( desktopWslConfig . distro ?? null )
846+ : stagedDesktopWslDistro ;
847+ const selectedDesktopWslDistro = effectiveDesktopWslDistro ?? defaultDesktopWslDistro ;
848+ const displayedDesktopWslDistroValue =
849+ effectiveDesktopWslDistro === defaultDesktopWslDistro ? "" : ( effectiveDesktopWslDistro ?? "" ) ;
850+ const selectableDesktopWslDistros = desktopWslDistros . filter (
851+ ( distro ) => distro . name !== defaultDesktopWslDistro ,
852+ ) ;
853+
854+ const normalizeDesktopWslConfig = useCallback (
855+ ( config : DesktopWslConfig ) : DesktopWslConfig => ( {
856+ enabled : config . enabled ,
857+ distro : config . distro === defaultDesktopWslDistro ? null : config . distro ,
858+ } ) ,
859+ [ defaultDesktopWslDistro ] ,
860+ ) ;
861+
862+ const leaveCurrentPrimaryThreadRoute = useCallback ( async ( ) => {
863+ const primaryEnvironmentId = readPrimaryEnvironmentDescriptor ( ) ?. environmentId ?? null ;
864+ if ( ! primaryEnvironmentId ) {
865+ return ;
866+ }
867+
868+ const routeEnvironmentId = pathname . split ( "/" ) . filter ( Boolean ) [ 0 ] ?? null ;
869+ if ( routeEnvironmentId === primaryEnvironmentId ) {
870+ await navigate ( { to : "/" , replace : true } ) ;
871+ }
872+ } , [ navigate , pathname ] ) ;
825873
826874 const handleDesktopServerExposureChange = useCallback (
827875 async ( checked : boolean ) => {
@@ -863,19 +911,30 @@ export function ConnectionsSettings() {
863911 async ( nextConfig : DesktopWslConfig ) => {
864912 if ( ! desktopBridge ) return ;
865913 setIsUpdatingDesktopWsl ( true ) ;
914+ setDesktopWslChangeStage ( "restarting-backend" ) ;
866915 setDesktopWslError ( null ) ;
867916 try {
868- const updated = await desktopBridge . wslSetConfig ( nextConfig ) ;
869- if ( ! updated ) {
870- throw new Error ( "The WSL backend configuration was rejected." ) ;
871- }
872- setDesktopWslConfig ( nextConfig ) ;
917+ const normalizedConfig = normalizeDesktopWslConfig ( nextConfig ) ;
918+ await leaveCurrentPrimaryThreadRoute ( ) ;
919+ await suppressWsConnectionLifecycle ( async ( ) => {
920+ const updated = await desktopBridge . wslSetConfig ( normalizedConfig ) ;
921+ if ( ! updated ) {
922+ throw new Error ( "The WSL backend configuration was rejected." ) ;
923+ }
924+ setDesktopWslChangeStage ( "reauthenticating" ) ;
925+ await refreshPrimaryEnvironmentDescriptor ( ) ;
926+ // Each backend signs sessions with its own key, so the OLD cookie is rejected
927+ // by the new backend (401). Re-bootstrap before any WS reconnect attempts.
928+ await reauthenticatePrimaryEnvironment ( ) ;
929+ } ) ;
930+ setDesktopWslConfig ( normalizedConfig ) ;
931+ setPendingDesktopWslConfig ( null ) ;
873932 toastManager . add ( {
874933 type : "success" ,
875934 title : "Backend restarted" ,
876- description : nextConfig . enabled
877- ? "The local backend is now launching inside WSL."
878- : "The local backend is now launching on Windows." ,
935+ description : normalizedConfig . enabled
936+ ? "The local backend is running inside WSL."
937+ : "The local backend is running on Windows." ,
879938 } ) ;
880939 } catch ( error ) {
881940 const message =
@@ -890,11 +949,17 @@ export function ConnectionsSettings() {
890949 ) ;
891950 } finally {
892951 setIsUpdatingDesktopWsl ( false ) ;
952+ setDesktopWslChangeStage ( null ) ;
893953 }
894954 } ,
895- [ desktopBridge ] ,
955+ [ desktopBridge , leaveCurrentPrimaryThreadRoute , normalizeDesktopWslConfig ] ,
896956 ) ;
897957
958+ const handleConfirmDesktopWslChange = useCallback ( ( ) => {
959+ if ( pendingDesktopWslConfig === null ) return ;
960+ void handleDesktopWslChange ( pendingDesktopWslConfig ) ;
961+ } , [ handleDesktopWslChange , pendingDesktopWslConfig ] ) ;
962+
898963 const handleRevokeDesktopPairingLink = useCallback ( async ( id : string ) => {
899964 setRevokingDesktopPairingLinkId ( id ) ;
900965 setDesktopAccessManagementError ( null ) ;
@@ -1197,6 +1262,18 @@ export function ConnectionsSettings() {
11971262 cancelled = true ;
11981263 } ;
11991264 } , [ desktopBridge ] ) ;
1265+
1266+ // Mirror the saved distro into the staged value on first load and whenever WSL is
1267+ // enabled — that way the dropdown shows the live choice while WSL is on, and falls
1268+ // back to the most-recent saved choice when the user toggles off. Once the user
1269+ // overrides the staged value while WSL is off, we leave it alone.
1270+ useEffect ( ( ) => {
1271+ if ( ! desktopWslConfig ) return ;
1272+ if ( ! stagedDesktopWslDistroInitializedRef . current || desktopWslConfig . enabled ) {
1273+ setStagedDesktopWslDistro ( desktopWslConfig . distro ) ;
1274+ stagedDesktopWslDistroInitializedRef . current = true ;
1275+ }
1276+ } , [ desktopWslConfig ] ) ;
12001277 const visibleDesktopPairingLinks = useMemo (
12011278 ( ) => desktopPairingLinks . filter ( ( pairingLink ) => pairingLink . role === "client" ) ,
12021279 [ desktopPairingLinks ] ,
@@ -1335,43 +1412,128 @@ export function ConnectionsSettings() {
13351412 ) : null
13361413 }
13371414 control = {
1338- < div className = "flex items-center gap-2" >
1339- < select
1340- className = "h-8 rounded-md border border-input bg-background px-2 text-sm"
1341- value = { desktopWslConfig ?. distro ?? "" }
1342- disabled = {
1343- ! desktopWslConfig || desktopWslDistros . length === 0 || isUpdatingDesktopWsl
1344- }
1345- onChange = { ( event ) => {
1346- void handleDesktopWslChange ( {
1347- enabled : desktopWslConfig ?. enabled ?? false ,
1348- distro : event . currentTarget . value || null ,
1349- } ) ;
1350- } }
1351- aria-label = "Select WSL distribution"
1352- >
1353- < option value = "" > Default distro</ option >
1354- { desktopWslDistros . map ( ( distro ) => (
1355- < option key = { distro . name } value = { distro . name } >
1356- { distro . name }
1357- { distro . isDefault ? " (default)" : "" }
1358- </ option >
1359- ) ) }
1360- </ select >
1361- < Switch
1362- checked = { desktopWslConfig ?. enabled ?? false }
1363- disabled = {
1364- ! desktopWslConfig || desktopWslDistros . length === 0 || isUpdatingDesktopWsl
1365- }
1366- onCheckedChange = { ( checked ) => {
1367- void handleDesktopWslChange ( {
1368- enabled : checked ,
1369- distro : desktopWslConfig ?. distro ?? null ,
1370- } ) ;
1371- } }
1372- aria-label = "Enable WSL backend"
1373- />
1374- </ div >
1415+ < AlertDialog
1416+ open = { pendingDesktopWslConfig !== null }
1417+ onOpenChange = { ( open ) => {
1418+ if ( isUpdatingDesktopWsl ) return ;
1419+ if ( ! open ) setPendingDesktopWslConfig ( null ) ;
1420+ } }
1421+ >
1422+ < div className = "flex items-center gap-2" >
1423+ < Select
1424+ value = { displayedDesktopWslDistroValue }
1425+ onValueChange = { ( value ) => {
1426+ const nextDistro = value || null ;
1427+ const normalizedDistro =
1428+ nextDistro === defaultDesktopWslDistro ? null : nextDistro ;
1429+ if ( ! desktopWslConfig ?. enabled ) {
1430+ setStagedDesktopWslDistro ( normalizedDistro ) ;
1431+ return ;
1432+ }
1433+ if ( normalizedDistro === desktopWslConfig . distro ) return ;
1434+ setPendingDesktopWslConfig ( {
1435+ enabled : true ,
1436+ distro : normalizedDistro ,
1437+ } ) ;
1438+ } }
1439+ >
1440+ < SelectTrigger
1441+ size = "sm"
1442+ className = "w-44"
1443+ aria-label = "Select WSL distribution"
1444+ disabled = {
1445+ ! desktopWslConfig ||
1446+ desktopWslDistros . length === 0 ||
1447+ isUpdatingDesktopWsl
1448+ }
1449+ >
1450+ < SelectValue >
1451+ { effectiveDesktopWslDistro &&
1452+ effectiveDesktopWslDistro !== defaultDesktopWslDistro
1453+ ? effectiveDesktopWslDistro
1454+ : defaultDesktopWslDistro
1455+ ? `${ defaultDesktopWslDistro } (default)`
1456+ : "WSL default" }
1457+ </ SelectValue >
1458+ </ SelectTrigger >
1459+ < SelectPopup align = "end" alignItemWithTrigger = { false } >
1460+ < SelectItem hideIndicator value = "" >
1461+ { defaultDesktopWslDistro
1462+ ? `${ defaultDesktopWslDistro } (default)`
1463+ : "WSL default" }
1464+ </ SelectItem >
1465+ { selectableDesktopWslDistros . map ( ( distro ) => (
1466+ < SelectItem key = { distro . name } hideIndicator value = { distro . name } >
1467+ { distro . name }
1468+ </ SelectItem >
1469+ ) ) }
1470+ </ SelectPopup >
1471+ </ Select >
1472+ < Switch
1473+ checked = { desktopWslConfig ?. enabled ?? false }
1474+ disabled = {
1475+ ! desktopWslConfig ||
1476+ desktopWslDistros . length === 0 ||
1477+ isUpdatingDesktopWsl
1478+ }
1479+ onCheckedChange = { ( checked ) => {
1480+ if ( ! desktopWslConfig ) return ;
1481+ const distro = checked
1482+ ? stagedDesktopWslDistro
1483+ : ( desktopWslConfig . distro ?? null ) ;
1484+ setPendingDesktopWslConfig (
1485+ normalizeDesktopWslConfig ( { enabled : checked , distro } ) ,
1486+ ) ;
1487+ } }
1488+ aria-label = "Enable WSL backend"
1489+ />
1490+ </ div >
1491+ < AlertDialogPopup >
1492+ < AlertDialogHeader >
1493+ < AlertDialogTitle >
1494+ { pendingDesktopWslConfig ?. enabled
1495+ ? desktopWslConfig ?. enabled
1496+ ? "Switch WSL distro?"
1497+ : "Enable WSL backend?"
1498+ : "Disable WSL backend?" }
1499+ </ AlertDialogTitle >
1500+ < AlertDialogDescription >
1501+ { pendingDesktopWslConfig ?. enabled
1502+ ? `T3 Code will restart the local backend inside ${
1503+ pendingDesktopWslConfig . distro ??
1504+ defaultDesktopWslDistro ??
1505+ "the WSL default distro"
1506+ } . This can take up to 30 seconds the first time. Each backend keeps its own threads — your Windows threads stay where they are and will return when you switch back.`
1507+ : "T3 Code will restart the local backend on Windows. This can take up to 30 seconds. Each backend keeps its own threads — your WSL threads stay where they are and will return when you switch back." }
1508+ </ AlertDialogDescription >
1509+ </ AlertDialogHeader >
1510+ < AlertDialogFooter >
1511+ < AlertDialogClose
1512+ disabled = { isUpdatingDesktopWsl }
1513+ render = { < Button variant = "outline" disabled = { isUpdatingDesktopWsl } /> }
1514+ >
1515+ Cancel
1516+ </ AlertDialogClose >
1517+ < Button
1518+ onClick = { handleConfirmDesktopWslChange }
1519+ disabled = { pendingDesktopWslConfig === null || isUpdatingDesktopWsl }
1520+ >
1521+ { isUpdatingDesktopWsl ? (
1522+ < >
1523+ < Spinner className = "size-3.5" />
1524+ { desktopWslChangeStage === "reauthenticating"
1525+ ? "Re-establishing session…"
1526+ : "Restarting backend…" }
1527+ </ >
1528+ ) : pendingDesktopWslConfig ?. enabled ? (
1529+ "Restart in WSL"
1530+ ) : (
1531+ "Restart on Windows"
1532+ ) }
1533+ </ Button >
1534+ </ AlertDialogFooter >
1535+ </ AlertDialogPopup >
1536+ </ AlertDialog >
13751537 }
13761538 />
13771539 ) : null }
0 commit comments