Skip to content

Commit f241be6

Browse files
committed
Polish WSL backend settings UX
- Replace the native <select> for distro selection with the design system's <Select> component (built on @base-ui/react/select), used elsewhere in settings. Fixes the slightly off-axis chevron and matches the dropdown styling of the rest of the panel. - Stop disabling the distro dropdown when WSL is off. The selection now stages locally (stagedDesktopWslDistro) without hitting the backend; flipping the toggle on uses the staged value as the new distro, while changes made while WSL is already enabled still go through the existing confirmation dialog. A useEffect mirrors the saved distro into the staged value on first load and whenever WSL is enabled. - Wire reauthenticatePrimaryEnvironment() into handleDesktopWslChange so the renderer re-exchanges the desktop bootstrap token for a session cookie signed by the new backend before any WS reconnect attempts. The call sits inside suppressWsConnectionLifecycle alongside the descriptor refresh so its 401-then-200 doesn't briefly surface as a connection hiccup in the UI. - Track a desktopWslChangeStage ('restarting-backend' / 'reauthenticating') so the dialog button reads "Restarting backend…" then "Re-establishing session…" instead of a single static "Restarting…" — gives the user feedback during the longer cold-launch path. - Expand the confirmation copy: set the time expectation ("can take up to 30 seconds the first time") and clarify that each backend keeps its own threads, so the previous list isn't gone — it returns when you switch back.
1 parent 6289f48 commit f241be6

1 file changed

Lines changed: 211 additions & 49 deletions

File tree

apps/web/src/components/settings/ConnectionsSettings.tsx

Lines changed: 211 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { 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";
34
import {
45
type AuthClientSession,
56
type AuthPairingLink,
@@ -41,6 +42,7 @@ import {
4142
} from "../ui/alert-dialog";
4243
import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
4344
import { QRCodeSvg } from "../ui/qr-code";
45+
import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
4446
import { Spinner } from "../ui/spinner";
4547
import { Switch } from "../ui/switch";
4648
import { stackedThreadToast, toastManager } from "../ui/toast";
@@ -51,6 +53,9 @@ import { setPairingTokenOnUrl } from "../../pairingUrl";
5153
import {
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

7379
const accessTimestampFormatter = new Intl.DateTimeFormat(undefined, {
7480
dateStyle: "medium",
@@ -758,6 +764,8 @@ function SavedBackendListRow({
758764
}
759765

760766
export 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

Comments
 (0)