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";...@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from "vite-plus/test";
3import {3import {
4 applyWslEnableSelection,4 applyWslEnableSelection,
5 isQrShareableEndpoint,5 isQrShareableEndpoint,
6 isWslSettingsRowVisible,
6 selectQrEndpointOption,7 selectQrEndpointOption,
7} from "./ConnectionsSettings.logic";8} from "./ConnectionsSettings.logic";
89
...@@ -15,6 +16,25 @@ const baseWslState: DesktopWslState = {...@@ -15,6 +16,25 @@ const baseWslState: DesktopWslState = {
15 preflightError: null,16 preflightError: null,
16};17};
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
18describe("applyWslEnableSelection", () => {38describe("applyWslEnableSelection", () => {
19 it("clears WSL-only and updates the distro before enabling both backends", async () => {39 it("clears WSL-only and updates the distro before enabling both backends", async () => {
20 const calls: Array<string> = [];40 const calls: Array<string> = [];
apps/web/src/components/settings/ConnectionsSettings.logic.ts+8
...@@ -11,6 +11,14 @@ export function isQrShareableEndpoint(endpoint: AdvertisedEndpoint): boolean {...@@ -11,6 +11,14 @@ export function isQrShareableEndpoint(endpoint: AdvertisedEndpoint): boolean {
11 return endpoint.status !== "unavailable" && endpoint.reachability !== "loopback";11 return endpoint.status !== "unavailable" && endpoint.reachability !== "loopback";
12}12}
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
14export type QrEndpointOption = {22export type QrEndpointOption = {
15 /** Unique per endpoint instance (AdvertisedEndpoint.id); safe as a React key. */23 /** Unique per endpoint instance (AdvertisedEndpoint.id); safe as a React key. */
16 readonly id: string;24 readonly id: string;
apps/web/src/components/settings/ConnectionsSettings.tsx+11-5
...@@ -43,6 +43,7 @@ import { resolveDesktopPairingUrl, resolveHostedPairingUrl } from "./pairingUrls...@@ -43,6 +43,7 @@ import { resolveDesktopPairingUrl, resolveHostedPairingUrl } from "./pairingUrls
43import {43import {
44 applyWslEnableSelection,44 applyWslEnableSelection,
45 isQrShareableEndpoint,45 isQrShareableEndpoint,
46 isWslSettingsRowVisible,
46 selectQrEndpointOption,47 selectQrEndpointOption,
47} from "./ConnectionsSettings.logic";48} from "./ConnectionsSettings.logic";
48import {49import {
...@@ -2766,10 +2767,13 @@ export function ConnectionsSettings() {...@@ -2766,10 +2767,13 @@ export function ConnectionsSettings() {
2766 // retry so the row doesn't flicker away, and the button reflects the2767 // retry so the row doesn't flicker away, and the button reflects the
2767 // loading state. With no error we simply haven't loaded yet (or WSL2768 // loading state. With no error we simply haven't loaded yet (or WSL
2768 // management isn't available), so render nothing.2769 // management isn't available), so render nothing.
2769 if (desktopWslError && canManageLocalBackend) {2770 if (
2771 isWslSettingsRowVisible({ state: null, error: desktopWslError }) &&
2772 canManageLocalBackend
2773 ) {
2770 return (2774 return (
2771 <SettingsRow2775 <SettingsRow
2772 title="WSL backend"2776 {...searchableSetting("wsl-backend")}
2773 description="Couldn't load the WSL backend state."2777 description="Couldn't load the WSL backend state."
2774 status={<span className="block text-destructive">{desktopWslError}</span>}2778 status={<span className="block text-destructive">{desktopWslError}</span>}
2775 control={2779 control={
...@@ -2794,11 +2798,13 @@ export function ConnectionsSettings() {...@@ -2794,11 +2798,13 @@ export function ConnectionsSettings() {
2794 // be stranded on a WSL preference they can't clear, so render a recovery2798 // be stranded on a WSL preference they can't clear, so render a recovery
2795 // row that switches back to Windows. When WSL is unavailable AND unused,2799 // row that switches back to Windows. When WSL is unavailable AND unused,
2796 // there's nothing to recover — keep the section hidden as before.2800 // there's nothing to recover — keep the section hidden as before.
2801 if (!isWslSettingsRowVisible({ state: desktopWslState, error: desktopWslError })) {
2802 return null;
2803 }
2797 if (!desktopWslState.available) {2804 if (!desktopWslState.available) {
2798 if (!desktopWslState.enabled && !desktopWslState.wslOnly) return null;
2799 return (2805 return (
2800 <SettingsRow2806 <SettingsRow
2801 title="WSL backend"2807 {...searchableSetting("wsl-backend")}
2802 description="WSL is no longer available, so the Windows backend is running instead. Switch off the WSL backend to clear this preference."2808 description="WSL is no longer available, so the Windows backend is running instead. Switch off the WSL backend to clear this preference."
2803 status={2809 status={
2804 desktopWslError ? (2810 desktopWslError ? (
...@@ -2835,7 +2841,7 @@ export function ConnectionsSettings() {...@@ -2835,7 +2841,7 @@ export function ConnectionsSettings() {
2835 return (2841 return (
2836 <>2842 <>
2837 <SettingsRow2843 <SettingsRow
2838 title="WSL backend"2844 {...searchableSetting("wsl-backend")}
2839 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."2845 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."
2840 status={2846 status={
2841 desktopWslError ? (2847 desktopWslError ? (
apps/web/src/components/settings/SettingsSidebarNav.tsx+22-1
...@@ -7,6 +7,8 @@ import {...@@ -7,6 +7,8 @@ import {
7 type ComponentType,7 type ComponentType,
8 type KeyboardEvent,8 type KeyboardEvent,
9} from "react";9} from "react";
10import { useEnvironmentQuery } from "~/state/query";
11import { desktopWslStateAtom } from "~/state/desktopWslState";
10import {12import {
11 ArchiveIcon,13 ArchiveIcon,
12 BlocksIcon,14 BlocksIcon,
...@@ -21,6 +23,8 @@ import {...@@ -21,6 +23,8 @@ import {
21} from "lucide-react";23} from "lucide-react";
22import { useLocation, useNavigate } from "@tanstack/react-router";24import { useLocation, useNavigate } from "@tanstack/react-router";
2325
26import { isElectron } from "~/env";
27import { isWslSettingsRowVisible } from "./ConnectionsSettings.logic";
24import { Button } from "../ui/button";28import { Button } from "../ui/button";
25import { Input } from "../ui/input";29import { Input } from "../ui/input";
26import { Kbd } from "../ui/kbd";30import { Kbd } from "../ui/kbd";
...@@ -38,6 +42,7 @@ import { SidebarUtilityMenu } from "../sidebar/SidebarChrome";...@@ -38,6 +42,7 @@ import { SidebarUtilityMenu } from "../sidebar/SidebarChrome";
38import { scrollToSettingsTarget } from "./settingsLayout";42import { scrollToSettingsTarget } from "./settingsLayout";
39import {43import {
40 searchSettings,44 searchSettings,
45 SETTINGS_SEARCH_ITEMS,
41 SETTINGS_SECTION_LABELS,46 SETTINGS_SECTION_LABELS,
42 type SettingsPath,47 type SettingsPath,
43 type SettingsSearchItem,48 type SettingsSearchItem,
...@@ -78,10 +83,26 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -78,10 +83,26 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
78 const searchInputRef = useRef<HTMLInputElement>(null);83 const searchInputRef = useRef<HTMLInputElement>(null);
79 const [query, setQuery] = useState("");84 const [query, setQuery] = useState("");
80 const [activeResultIndex, setActiveResultIndex] = useState(0);85 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]);
82 const isSearching = query.trim().length > 0;99 const isSearching = query.trim().length > 0;
83 const hasResults = results.length > 0;100 const hasResults = results.length > 0;
84101
102 useEffect(() => {
103 setActiveResultIndex((index) => Math.min(index, Math.max(results.length - 1, 0)));
104 }, [results.length]);
105
85 useEffect(() => {106 useEffect(() => {
86 const result = results[activeResultIndex];107 const result = results[activeResultIndex];
87 if (!result) return;108 if (!result) return;
apps/web/src/components/settings/settingsSearch.test.ts+11
...@@ -71,6 +71,17 @@ describe("searchSettings", () => {...@@ -71,6 +71,17 @@ describe("searchSettings", () => {
71 it("hides desktop-only settings from browser search", () => {71 it("hides desktop-only settings from browser search", () => {
72 expect(SETTINGS_SEARCH_ITEMS.some((item) => item.id === "quit-confirmation")).toBe(true);72 expect(SETTINGS_SEARCH_ITEMS.some((item) => item.id === "quit-confirmation")).toBe(true);
73 expect(searchSettings("quit confirmation")).toEqual([]);73 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 });
74 });85 });
7586
76 it("keeps catalog result ids unique", () => {87 it("keeps catalog result ids unique", () => {
apps/web/src/components/settings/settingsSearch.ts+13
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import { isElectron } from "~/env";1import { isElectron } from "~/env";
2import { isWindowsPlatform } from "~/lib/utils";
23
3export type SettingsPath =4export type SettingsPath =
4 | "/settings/general"5 | "/settings/general"
...@@ -18,6 +19,9 @@ export interface SettingsSearchItem {...@@ -18,6 +19,9 @@ export interface SettingsSearchItem {
18 // Its row only renders in the desktop app, so a browser result would land on19 // Its row only renders in the desktop app, so a browser result would land on
19 // an anchor that isn't there.20 // an anchor that isn't there.
20 readonly desktopOnly?: boolean;21 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;
21}25}
2226
23/**27/**
...@@ -259,6 +263,13 @@ export const SETTINGS_SEARCH_ITEMS = [...@@ -259,6 +263,13 @@ export const SETTINGS_SEARCH_ITEMS = [
259 title: "Remote environments",263 title: "Remote environments",
260 to: "/settings/connections",264 to: "/settings/connections",
261 },265 },
266 {
267 id: "wsl-backend",
268 title: "WSL backend",
269 to: "/settings/connections",
270 desktopOnly: true,
271 windowsOnly: true,
272 },
262 {273 {
263 id: "archive",274 id: "archive",
264 title: "Archived threads",275 title: "Archived threads",
...@@ -304,6 +315,8 @@ export function searchSettings(...@@ -304,6 +315,8 @@ export function searchSettings(
304 return items.filter(315 return items.filter(
305 (item) =>316 (item) =>
306 (isElectron || item.desktopOnly !== true) &&317 (isElectron || item.desktopOnly !== true) &&
318 (!item.windowsOnly ||
319 isWindowsPlatform(typeof navigator === "undefined" ? "" : navigator.platform)) &&
307 normalizeSearchText(item.title).includes(normalizedQuery),320 normalizeSearchText(item.title).includes(normalizedQuery),
308 );321 );
309}322}