authorgravatar for 73941998+UtkarshUsername@users.noreply.github.comUtkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com> 2026-09-01 05:19:16+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-31 16:49:16-07:00
logc50b0b4ef8d61ffe3de1cfa6249f906709f7da1f
treebeb27ec08608ad752946d5ab05793e25b6b4408b
parent929f7e6479d00754dee4e4554b24b25478b8064d
signature Signed by PGP key B5690EEEBB952194

fix(web): make WSL settings searchable (#8881)


6 files changed, 85 insertions(+), 6 deletions(-)

apps/web/src/components/settings/ConnectionsSettings.logic.test.ts+20
......@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from "vite-plus/test";
33import {
44 applyWslEnableSelection,
55 isQrShareableEndpoint,
6 isWslSettingsRowVisible,
67 selectQrEndpointOption,
78} from "./ConnectionsSettings.logic";
89
......@@ -15,6 +16,25 @@ const baseWslState: DesktopWslState = {
1516 preflightError: null,
1617};
1718
19describe("isWslSettingsRowVisible", () => {
20 it("shows the retry row when the WSL state failed to load", () => {
21 expect(isWslSettingsRowVisible({ state: null, error: "load failed" })).toBe(true);
22 });
23
24 it("hides an unavailable and unused WSL snapshot", () => {
25 expect(
26 isWslSettingsRowVisible({
27 state: { ...baseWslState, available: false, wslOnly: false },
28 error: null,
29 }),
30 ).toBe(false);
31 });
32
33 it("shows an available WSL snapshot", () => {
34 expect(isWslSettingsRowVisible({ state: baseWslState, error: null })).toBe(true);
35 });
36});
37
1838describe("applyWslEnableSelection", () => {
1939 it("clears WSL-only and updates the distro before enabling both backends", async () => {
2040 const calls: Array<string> = [];
apps/web/src/components/settings/ConnectionsSettings.logic.ts+8
......@@ -11,6 +11,14 @@ export function isQrShareableEndpoint(endpoint: AdvertisedEndpoint): boolean {
1111 return endpoint.status !== "unavailable" && endpoint.reachability !== "loopback";
1212}
1313
14export function isWslSettingsRowVisible(input: {
15 readonly state: DesktopWslState | null;
16 readonly error: string | null;
17}): boolean {
18 const { state, error } = input;
19 return state ? state.available || state.enabled || state.wslOnly : error !== null;
20}
21
1422export type QrEndpointOption = {
1523 /** Unique per endpoint instance (AdvertisedEndpoint.id); safe as a React key. */
1624 readonly id: string;
apps/web/src/components/settings/ConnectionsSettings.tsx+11-5
......@@ -43,6 +43,7 @@ import { resolveDesktopPairingUrl, resolveHostedPairingUrl } from "./pairingUrls
4343import {
4444 applyWslEnableSelection,
4545 isQrShareableEndpoint,
46 isWslSettingsRowVisible,
4647 selectQrEndpointOption,
4748} from "./ConnectionsSettings.logic";
4849import {
......@@ -2766,10 +2767,13 @@ export function ConnectionsSettings() {
27662767 // retry so the row doesn't flicker away, and the button reflects the
27672768 // loading state. With no error we simply haven't loaded yet (or WSL
27682769 // management isn't available), so render nothing.
2769 if (desktopWslError && canManageLocalBackend) {
2770 if (
2771 isWslSettingsRowVisible({ state: null, error: desktopWslError }) &&
2772 canManageLocalBackend
2773 ) {
27702774 return (
27712775 <SettingsRow
2772 title="WSL backend"
2776 {...searchableSetting("wsl-backend")}
27732777 description="Couldn't load the WSL backend state."
27742778 status={<span className="block text-destructive">{desktopWslError}</span>}
27752779 control={
......@@ -2794,11 +2798,13 @@ export function ConnectionsSettings() {
27942798 // be stranded on a WSL preference they can't clear, so render a recovery
27952799 // row that switches back to Windows. When WSL is unavailable AND unused,
27962800 // there's nothing to recover — keep the section hidden as before.
2801 if (!isWslSettingsRowVisible({ state: desktopWslState, error: desktopWslError })) {
2802 return null;
2803 }
27972804 if (!desktopWslState.available) {
2798 if (!desktopWslState.enabled && !desktopWslState.wslOnly) return null;
27992805 return (
28002806 <SettingsRow
2801 title="WSL backend"
2807 {...searchableSetting("wsl-backend")}
28022808 description="WSL is no longer available, so the Windows backend is running instead. Switch off the WSL backend to clear this preference."
28032809 status={
28042810 desktopWslError ? (
......@@ -2835,7 +2841,7 @@ export function ConnectionsSettings() {
28352841 return (
28362842 <>
28372843 <SettingsRow
2838 title="WSL backend"
2844 {...searchableSetting("wsl-backend")}
28392845 description="Run a second backend inside a WSL distro alongside the Windows one. Pick a distro to start it; pick Off to stop it. Projects opened against the WSL backend live on the Linux side; Windows projects stay where they are."
28402846 status={
28412847 desktopWslError ? (
apps/web/src/components/settings/SettingsSidebarNav.tsx+22-1
......@@ -7,6 +7,8 @@ import {
77 type ComponentType,
88 type KeyboardEvent,
99} from "react";
10import { useEnvironmentQuery } from "~/state/query";
11import { desktopWslStateAtom } from "~/state/desktopWslState";
1012import {
1113 ArchiveIcon,
1214 BlocksIcon,
......@@ -21,6 +23,8 @@ import {
2123} from "lucide-react";
2224import { useLocation, useNavigate } from "@tanstack/react-router";
2325
26import { isElectron } from "~/env";
27import { isWslSettingsRowVisible } from "./ConnectionsSettings.logic";
2428import { Button } from "../ui/button";
2529import { Input } from "../ui/input";
2630import { Kbd } from "../ui/kbd";
......@@ -38,6 +42,7 @@ import { SidebarUtilityMenu } from "../sidebar/SidebarChrome";
3842import { scrollToSettingsTarget } from "./settingsLayout";
3943import {
4044 searchSettings,
45 SETTINGS_SEARCH_ITEMS,
4146 SETTINGS_SECTION_LABELS,
4247 type SettingsPath,
4348 type SettingsSearchItem,
......@@ -78,10 +83,26 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
7883 const searchInputRef = useRef<HTMLInputElement>(null);
7984 const [query, setQuery] = useState("");
8085 const [activeResultIndex, setActiveResultIndex] = useState(0);
81 const results = useMemo(() => searchSettings(query), [query]);
86 const desktopWsl = useEnvironmentQuery(isElectron ? desktopWslStateAtom : null);
87 const searchableItems = useMemo(() => {
88 const wslState = desktopWsl.data;
89 const rowRenders = isWslSettingsRowVisible({
90 state: wslState,
91 error: desktopWsl.error,
92 });
93 if (rowRenders) {
94 return SETTINGS_SEARCH_ITEMS;
95 }
96 return SETTINGS_SEARCH_ITEMS.filter((item) => item.id !== "wsl-backend");
97 }, [desktopWsl.data, desktopWsl.error]);
98 const results = useMemo(() => searchSettings(query, searchableItems), [query, searchableItems]);
8299 const isSearching = query.trim().length > 0;
83100 const hasResults = results.length > 0;
84101
102 useEffect(() => {
103 setActiveResultIndex((index) => Math.min(index, Math.max(results.length - 1, 0)));
104 }, [results.length]);
105
85106 useEffect(() => {
86107 const result = results[activeResultIndex];
87108 if (!result) return;
apps/web/src/components/settings/settingsSearch.test.ts+11
......@@ -71,6 +71,17 @@ describe("searchSettings", () => {
7171 it("hides desktop-only settings from browser search", () => {
7272 expect(SETTINGS_SEARCH_ITEMS.some((item) => item.id === "quit-confirmation")).toBe(true);
7373 expect(searchSettings("quit confirmation")).toEqual([]);
74 expect(searchSettings("wsl")).toEqual([]);
75 });
76
77 it("registers the WSL backend as a desktop-only setting", () => {
78 expect(SETTINGS_SEARCH_ITEMS).toContainEqual({
79 id: "wsl-backend",
80 title: "WSL backend",
81 to: "/settings/connections",
82 desktopOnly: true,
83 windowsOnly: true,
84 });
7485 });
7586
7687 it("keeps catalog result ids unique", () => {
apps/web/src/components/settings/settingsSearch.ts+13
......@@ -1,4 +1,5 @@
11import { isElectron } from "~/env";
2import { isWindowsPlatform } from "~/lib/utils";
23
34export type SettingsPath =
45 | "/settings/general"
......@@ -18,6 +19,9 @@ export interface SettingsSearchItem {
1819 // Its row only renders in the desktop app, so a browser result would land on
1920 // an anchor that isn't there.
2021 readonly desktopOnly?: boolean;
22 // Its row only renders on Windows desktop, so other desktop platforms must
23 // not expose a result that points to a missing anchor.
24 readonly windowsOnly?: boolean;
2125}
2226
2327/**
......@@ -259,6 +263,13 @@ export const SETTINGS_SEARCH_ITEMS = [
259263 title: "Remote environments",
260264 to: "/settings/connections",
261265 },
266 {
267 id: "wsl-backend",
268 title: "WSL backend",
269 to: "/settings/connections",
270 desktopOnly: true,
271 windowsOnly: true,
272 },
262273 {
263274 id: "archive",
264275 title: "Archived threads",
......@@ -304,6 +315,8 @@ export function searchSettings(
304315 return items.filter(
305316 (item) =>
306317 (isElectron || item.desktopOnly !== true) &&
318 (!item.windowsOnly ||
319 isWindowsPlatform(typeof navigator === "undefined" ? "" : navigator.platform)) &&
307320 normalizeSearchText(item.title).includes(normalizedQuery),
308321 );
309322}