authorgravatar for 73941998+UtkarshUsername@users.noreply.github.comUtkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com> 2026-09-06 21:33:27+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-06 21:33:27+05:30
log72cb638a8a6656d0b89899d9d62afaed0c2f7e5f
tree417a0ac58076b2271e202d63881b9a8aaaf813e3
parent223ff4490f764a74ff911589e97b9bbcd595fee8
signature Signed by PGP key B5690EEEBB952194

fix(web): show Tux icon for WSL environments (#8511)


11 files changed, 94 insertions(+), 19 deletions(-)

apps/mobile/src/components/EnvironmentMachineSymbol.tsx+2
...@@ -6,6 +6,7 @@ import { SymbolView } from "./AppSymbol";...@@ -6,6 +6,7 @@ import { SymbolView } from "./AppSymbol";
6const SYMBOL_BY_KIND: Record<EnvironmentMachineKind, SFSymbol> = {6const SYMBOL_BY_KIND: Record<EnvironmentMachineKind, SFSymbol> = {
7 server: "server.rack",7 server: "server.rack",
8 cloud: "cloud",8 cloud: "cloud",
9 linux: "terminal",
9 desktop: "desktopcomputer",10 desktop: "desktopcomputer",
10 laptop: "laptopcomputer",11 laptop: "laptopcomputer",
11 "mac-mini": "macmini",12 "mac-mini": "macmini",
...@@ -15,6 +16,7 @@ const SYMBOL_BY_KIND: Record<EnvironmentMachineKind, SFSymbol> = {...@@ -15,6 +16,7 @@ const SYMBOL_BY_KIND: Record<EnvironmentMachineKind, SFSymbol> = {
15export const ENVIRONMENT_MACHINE_KIND_LABELS: Record<EnvironmentMachineKind, string> = {16export const ENVIRONMENT_MACHINE_KIND_LABELS: Record<EnvironmentMachineKind, string> = {
16 server: "Server",17 server: "Server",
17 cloud: "Cloud VM",18 cloud: "Cloud VM",
19 linux: "Linux/WSL",
18 desktop: "Desktop",20 desktop: "Desktop",
19 laptop: "Laptop",21 laptop: "Laptop",
20 "mac-mini": "Mac mini",22 "mac-mini": "Mac mini",
apps/server/src/environment/ServerEnvironmentMachine.test.ts+20
...@@ -202,6 +202,26 @@ describe("detectServerEnvironmentMachineKind", () => {...@@ -202,6 +202,26 @@ describe("detectServerEnvironmentMachineKind", () => {
202 }),202 }),
203 );203 );
204204
205 it.effect("recognizes WSL before its Hyper-V DMI identity", () =>
206 Effect.gen(function* () {
207 const result = yield* detectServerEnvironmentMachineKind().pipe(
208 Effect.provide(
209 withPlatform(
210 "linux",
211 dmiFileSystem({
212 osrelease: "5.15.153.1-microsoft-standard-WSL2\n",
213 chassis_type: "3\n",
214 sys_vendor: "Microsoft Corporation\n",
215 product_name: "Virtual Machine\n",
216 }),
217 ),
218 ),
219 );
220
221 expect(result).toBe("linux");
222 }),
223 );
224
205 it.effect("returns null on Linux without DMI (containers, ARM boards)", () =>225 it.effect("returns null on Linux without DMI (containers, ARM boards)", () =>
206 Effect.gen(function* () {226 Effect.gen(function* () {
207 const result = yield* detectServerEnvironmentMachineKind().pipe(227 const result = yield* detectServerEnvironmentMachineKind().pipe(
apps/server/src/environment/ServerEnvironmentMachine.ts+8-1
...@@ -12,6 +12,7 @@ import * as ProcessRunner from "../processRunner.ts";...@@ -12,6 +12,7 @@ import * as ProcessRunner from "../processRunner.ts";
12 */12 */
1313
14const DMI_ROOT = "/sys/class/dmi/id";14const DMI_ROOT = "/sys/class/dmi/id";
15const KERNEL_RELEASE_PATH = "/proc/sys/kernel/osrelease";
1516
16// SMBIOS 3.x System Enclosure types (table 17). Codes that describe a shape17// SMBIOS 3.x System Enclosure types (table 17). Codes that describe a shape
17// rather than a machine (docking stations, blades enclosures, IoT gateways)18// rather than a machine (docking stations, blades enclosures, IoT gateways)
...@@ -144,11 +145,17 @@ const detectDarwinMachineKind = Effect.fn("detectDarwinMachineKind")(function* (...@@ -144,11 +145,17 @@ const detectDarwinMachineKind = Effect.fn("detectDarwinMachineKind")(function* (
144});145});
145146
146const detectLinuxMachineKind = Effect.fn("detectLinuxMachineKind")(function* () {147const detectLinuxMachineKind = Effect.fn("detectLinuxMachineKind")(function* () {
147 const [chassisType, sysVendor, productName] = yield* Effect.all([148 const [kernelRelease, chassisType, sysVendor, productName] = yield* Effect.all([
149 readOptionalFile(KERNEL_RELEASE_PATH),
148 readOptionalFile(`${DMI_ROOT}/chassis_type`),150 readOptionalFile(`${DMI_ROOT}/chassis_type`),
149 readOptionalFile(`${DMI_ROOT}/sys_vendor`),151 readOptionalFile(`${DMI_ROOT}/sys_vendor`),
150 readOptionalFile(`${DMI_ROOT}/product_name`),152 readOptionalFile(`${DMI_ROOT}/product_name`),
151 ]);153 ]);
154 // WSL exposes Microsoft in its kernel release on both WSL 1 and WSL 2.
155 // Check it before DMI because WSL 2 presents as a Hyper-V VM.
156 if (kernelRelease?.toLowerCase().includes("microsoft")) {
157 return "linux";
158 }
152 return machineKindFromDmi({ chassisType, sysVendor, productName });159 return machineKindFromDmi({ chassisType, sysVendor, productName });
153});160});
154161
apps/web/src/components/EnvironmentMachineIcon.tsx+3
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import type { EnvironmentMachineKind } from "@t3tools/contracts";1import type { EnvironmentMachineKind } from "@t3tools/contracts";
2import { CloudIcon, LaptopIcon, MonitorIcon, ServerIcon, type LucideProps } from "lucide-react";2import { CloudIcon, LaptopIcon, MonitorIcon, ServerIcon, type LucideProps } from "lucide-react";
3import type { FunctionComponent, SVGProps } from "react";3import type { FunctionComponent, SVGProps } from "react";
4import { LinuxIcon } from "./Icons";
45
5// Lucide has no Apple desktops, so these two are drawn to its grammar (246// Lucide has no Apple desktops, so these two are drawn to its grammar (24
6// unit grid, 2 unit stroke, round joins) and share its prop surface so callers7// unit grid, 2 unit stroke, round joins) and share its prop surface so callers
...@@ -45,6 +46,7 @@ function MacStudioIcon(props: SVGProps<SVGSVGElement>) {...@@ -45,6 +46,7 @@ function MacStudioIcon(props: SVGProps<SVGSVGElement>) {
45const ICON_BY_KIND: Record<EnvironmentMachineKind, FunctionComponent<LucideProps>> = {46const ICON_BY_KIND: Record<EnvironmentMachineKind, FunctionComponent<LucideProps>> = {
46 server: ServerIcon,47 server: ServerIcon,
47 cloud: CloudIcon,48 cloud: CloudIcon,
49 linux: LinuxIcon,
48 desktop: MonitorIcon,50 desktop: MonitorIcon,
49 laptop: LaptopIcon,51 laptop: LaptopIcon,
50 "mac-mini": MacMiniIcon,52 "mac-mini": MacMiniIcon,
...@@ -54,6 +56,7 @@ const ICON_BY_KIND: Record<EnvironmentMachineKind, FunctionComponent<LucideProps...@@ -54,6 +56,7 @@ const ICON_BY_KIND: Record<EnvironmentMachineKind, FunctionComponent<LucideProps
54export const ENVIRONMENT_MACHINE_KIND_LABELS: Record<EnvironmentMachineKind, string> = {56export const ENVIRONMENT_MACHINE_KIND_LABELS: Record<EnvironmentMachineKind, string> = {
55 server: "Server",57 server: "Server",
56 cloud: "Cloud VM",58 cloud: "Cloud VM",
59 linux: "Linux/WSL",
57 desktop: "Desktop",60 desktop: "Desktop",
58 laptop: "Laptop",61 laptop: "Laptop",
59 "mac-mini": "Mac mini",62 "mac-mini": "Mac mini",
apps/web/src/components/Icons.tsx+6-1
...@@ -1,8 +1,13 @@...@@ -1,8 +1,13 @@
1import React, { type SVGProps, useId } from "react";1import React, { type SVGProps, useId } from "react";
2import { cn } from "~/lib/utils";2import { cn } from "~/lib/utils";
3
4export type Icon = React.FC<SVGProps<SVGSVGElement>>;3export type Icon = React.FC<SVGProps<SVGSVGElement>>;
54
5export const LinuxIcon: Icon = ({ className, ...props }) => (
6 <svg {...props} className={className} viewBox="-2 -2 28 28" fill="currentColor">
7 <path d="M12.504 0c-.155 0-.315.008-.48.021-4.226.333-3.105 4.807-3.17 6.298-.076 1.092-.3 1.953-1.05 3.02-.885 1.051-2.127 2.75-2.716 4.521-.278.832-.41 1.684-.287 2.489a.424.424 0 00-.11.135c-.26.268-.45.6-.663.839-.199.199-.485.267-.797.4-.313.136-.658.269-.864.68-.09.189-.136.394-.132.602 0 .199.027.4.055.536.058.399.116.728.04.97-.249.68-.28 1.145-.106 1.484.174.334.535.47.94.601.81.2 1.91.135 2.774.6.926.466 1.866.67 2.616.47.526-.116.97-.464 1.208-.946.587-.003 1.23-.269 2.26-.334.699-.058 1.574.267 2.577.2.025.134.063.198.114.333l.003.003c.391.778 1.113 1.132 1.884 1.071.771-.06 1.592-.536 2.257-1.306.631-.765 1.683-1.084 2.378-1.503.348-.199.629-.469.649-.853.023-.4-.2-.811-.714-1.376v-.097l-.003-.003c-.17-.2-.25-.535-.338-.926-.085-.401-.182-.786-.492-1.046h-.003c-.059-.054-.123-.067-.188-.135a.357.357 0 00-.19-.064c.431-1.278.264-2.55-.173-3.694-.533-1.41-1.465-2.638-2.175-3.483-.796-1.005-1.576-1.957-1.56-3.368.026-2.152.236-6.133-3.544-6.139zm.529 3.405h.013c.213 0 .396.062.584.198.19.135.33.332.438.533.105.259.158.459.166.724 0-.02.006-.04.006-.06v.105a.086.086 0 01-.004-.021l-.004-.024a1.807 1.807 0 01-.15.706.953.953 0 01-.213.335.71.71 0 00-.088-.042c-.104-.045-.198-.064-.284-.133a1.312 1.312 0 00-.22-.066c.05-.06.146-.133.183-.198.053-.128.082-.264.088-.402v-.02a1.21 1.21 0 00-.061-.4c-.045-.134-.101-.2-.183-.333-.084-.066-.167-.132-.267-.132h-.016c-.093 0-.176.03-.262.132a.8.8 0 00-.205.334 1.18 1.18 0 00-.09.4v.019c.002.089.008.179.02.267-.193-.067-.438-.135-.607-.202a1.635 1.635 0 01-.018-.2v-.02a1.772 1.772 0 01.15-.768c.082-.22.232-.406.43-.533a.985.985 0 01.594-.2zm-2.962.059h.036c.142 0 .27.048.399.135.146.129.264.288.344.465.09.199.14.4.153.667v.004c.007.134.006.2-.002.266v.08c-.03.007-.056.018-.083.024-.152.055-.274.135-.393.2.012-.09.013-.18.003-.267v-.015c-.012-.133-.04-.2-.082-.333a.613.613 0 00-.166-.267.248.248 0 00-.183-.064h-.021c-.071.006-.13.04-.186.132a.552.552 0 00-.12.27.944.944 0 00-.023.33v.015c.012.135.037.2.08.334.046.134.098.2.166.268.01.009.02.018.034.024-.07.057-.117.07-.176.136a.304.304 0 01-.131.068 2.62 2.62 0 01-.275-.402 1.772 1.772 0 01-.155-.667 1.759 1.759 0 01.08-.668 1.43 1.43 0 01.283-.535c.128-.133.26-.2.418-.2zm1.37 1.706c.332 0 .733.065 1.216.399.293.2.523.269 1.052.468h.003c.255.136.405.266.478.399v-.131a.571.571 0 01.016.47c-.123.31-.516.643-1.063.842v.002c-.268.135-.501.333-.775.465-.276.135-.588.292-1.012.267a1.139 1.139 0 01-.448-.067 3.566 3.566 0 01-.322-.198c-.195-.135-.363-.332-.612-.465v-.005h-.005c-.4-.246-.616-.512-.686-.71-.07-.268-.005-.47.193-.6.224-.135.38-.271.483-.336.104-.074.143-.102.176-.131h.002v-.003c.169-.202.436-.47.839-.601.139-.036.294-.065.466-.065zm2.8 2.142c.358 1.417 1.196 3.475 1.735 4.473.286.534.855 1.659 1.102 3.024.156-.005.33.018.513.064.646-1.671-.546-3.467-1.089-3.966-.22-.2-.232-.335-.123-.335.59.534 1.365 1.572 1.646 2.757.13.535.16 1.104.021 1.67.067.028.135.06.205.067 1.032.534 1.413.938 1.23 1.537v-.043c-.06-.003-.12 0-.18 0h-.016c.151-.467-.182-.825-1.065-1.224-.915-.4-1.646-.336-1.77.465-.008.043-.013.066-.018.135-.068.023-.139.053-.209.064-.43.268-.662.669-.793 1.187-.13.533-.17 1.156-.205 1.869v.003c-.02.334-.17.838-.319 1.35-1.5 1.072-3.58 1.538-5.348.334a2.645 2.645 0 00-.402-.533 1.45 1.45 0 00-.275-.333c.182 0 .338-.03.465-.067a.615.615 0 00.314-.334c.108-.267 0-.697-.345-1.163-.345-.467-.931-.995-1.788-1.521-.63-.4-.986-.87-1.15-1.396-.165-.534-.143-1.085-.015-1.645.245-1.07.873-2.11 1.274-2.763.107-.065.037.135-.408.974-.396.751-1.14 2.497-.122 3.854a8.123 8.123 0 01.647-2.876c.564-1.278 1.743-3.504 1.836-5.268.048.036.217.135.289.202.218.133.38.333.59.465.21.201.477.335.876.335.039.003.075.006.11.006.412 0 .73-.134.997-.268.29-.134.52-.334.74-.4h.005c.467-.135.835-.402 1.044-.7zm2.185 8.958c.037.6.343 1.245.882 1.377.588.134 1.434-.333 1.791-.765l.211-.01c.315-.007.577.01.847.268l.003.003c.208.199.305.53.391.876.085.4.154.78.409 1.066.486.527.645.906.636 1.14l.003-.007v.018l-.003-.012c-.015.262-.185.396-.498.595-.63.401-1.746.712-2.457 1.57-.618.737-1.37 1.14-2.036 1.191-.664.053-1.237-.2-1.574-.898l-.005-.003c-.21-.4-.12-1.025.056-1.69.176-.668.428-1.344.463-1.897.037-.714.076-1.335.195-1.814.12-.465.308-.797.641-.984l.045-.022zm-10.814.049h.01c.053 0 .105.005.157.014.376.055.706.333 1.023.752l.91 1.664.003.003c.243.533.754 1.064 1.189 1.637.434.598.77 1.131.729 1.57v.006c-.057.744-.48 1.148-1.125 1.294-.645.135-1.52.002-2.395-.464-.968-.536-2.118-.469-2.857-.602-.369-.066-.61-.2-.723-.4-.11-.2-.113-.602.123-1.23v-.004l.002-.003c.117-.334.03-.752-.027-1.118-.055-.401-.083-.71.043-.94.16-.334.396-.4.69-.533.294-.135.64-.202.915-.47h.002v-.002c.256-.268.445-.601.668-.838.19-.201.38-.336.663-.336zm7.159-9.074c-.435.201-.945.535-1.488.535-.542 0-.97-.267-1.28-.466-.154-.134-.28-.268-.373-.335-.164-.134-.144-.333-.074-.333.109.016.129.134.199.2.096.066.215.2.36.333.292.2.68.467 1.167.467.485 0 1.053-.267 1.398-.466.195-.135.445-.334.648-.467.156-.136.149-.267.279-.267.128.016.034.134-.147.332a8.097 8.097 0 01-.69.468zm-1.082-1.583V5.64c-.006-.02.013-.042.029-.05.074-.043.18-.027.26.004.063 0 .16.067.15.135-.006.049-.085.066-.135.066-.055 0-.092-.043-.141-.068-.052-.018-.146-.008-.163-.065zm-.551 0c-.02.058-.113.049-.166.066-.047.025-.086.068-.14.068-.05 0-.13-.02-.136-.068-.01-.066.088-.133.15-.133.08-.031.184-.047.259-.005.019.009.036.03.03.05v.02h.003z" />
8 </svg>
9);
10
6export const GitHubIcon: Icon = (props) => (11export const GitHubIcon: Icon = (props) => (
7 <svg {...props} viewBox="0 0 1024 1024" fill="none">12 <svg {...props} viewBox="0 0 1024 1024" fill="none">
8 <path13 <path
apps/web/src/components/LegacySidebar.tsx+22-12
...@@ -3,8 +3,6 @@ import {...@@ -3,8 +3,6 @@ import {
3 ArchiveIcon,3 ArchiveIcon,
4 ArrowUpDownIcon,4 ArrowUpDownIcon,
5 ChevronRightIcon,5 ChevronRightIcon,
6 CloudIcon,
7 ContainerIcon,
8 FolderPlusIcon,6 FolderPlusIcon,
9 Globe2Icon,7 Globe2Icon,
10 SearchIcon,8 SearchIcon,
...@@ -72,7 +70,7 @@ import {...@@ -72,7 +70,7 @@ import {
72 type SidebarThreadPreviewCount,70 type SidebarThreadPreviewCount,
73 type SidebarThreadSortOrder,71 type SidebarThreadSortOrder,
74} from "@t3tools/contracts/settings";72} from "@t3tools/contracts/settings";
75import { isDesktopLocalConnectionTarget } from "../connection/desktopLocal";73import { isDesktopLocalConnectionTarget, isWslConnectionTarget } from "../connection/desktopLocal";
76import { useDesktopLocalBootstraps } from "../connection/useDesktopLocalBootstraps";74import { useDesktopLocalBootstraps } from "../connection/useDesktopLocalBootstraps";
77import { isElectron } from "../env";75import { isElectron } from "../env";
78import { useTerminalFocus } from "../hooks/useTerminalFocus";76import { useTerminalFocus } from "../hooks/useTerminalFocus";
...@@ -402,9 +400,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP...@@ -402,9 +400,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
402 const remoteMachine = resolveEnvironmentMachineKind(environment?.serverConfig ?? null);400 const remoteMachine = resolveEnvironmentMachineKind(environment?.serverConfig ?? null);
403 // A desktop-local secondary backend (e.g. the WSL backend) shows up as a401 // A desktop-local secondary backend (e.g. the WSL backend) shows up as a
404 // bearer environment whose connection id is prefixed "local:". It runs on the402 // bearer environment whose connection id is prefixed "local:". It runs on the
405 // user's own machine, so the cloud icon is misleading — label it "Local" and403 // user's own machine, so the cloud icon is misleading, label it "Local" and
406 // suppress the cloud icon (the project header already shows a container icon404 // suppress the cloud icon (the project header already shows a
407 // for desktop-local projects, see sidebarProjectGrouping).405 // local-environment icon for desktop-local projects, see sidebarProjectGrouping).
408 const isDesktopLocalThread =406 const isDesktopLocalThread =
409 environment !== null && isDesktopLocalConnectionTarget(environment.entry.target);407 environment !== null && isDesktopLocalConnectionTarget(environment.entry.target);
410 const threadEnvironmentLabel = isRemoteThread408 const threadEnvironmentLabel = isRemoteThread
...@@ -1109,6 +1107,11 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec...@@ -1109,6 +1107,11 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
1109 isManualProjectSorting,1107 isManualProjectSorting,
1110 dragHandleProps,1108 dragHandleProps,
1111 } = props;1109 } = props;
1110 const environmentMachine = project.allRemoteMembersAreWsl
1111 ? "linux"
1112 : project.allRemoteMembersAreDesktopLocal
1113 ? "laptop"
1114 : "cloud";
1112 const threadSortOrder = useClientSettings<SidebarThreadSortOrder>(1115 const threadSortOrder = useClientSettings<SidebarThreadSortOrder>(
1113 (settings) => settings.sidebarThreadSortOrder,1116 (settings) => settings.sidebarThreadSortOrder,
1114 );1117 );
...@@ -2351,15 +2354,11 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec...@@ -2351,15 +2354,11 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
2351 ? "Local sandbox project"2354 ? "Local sandbox project"
2352 : "Remote project"2355 : "Remote project"
2353 }2356 }
2354 className="pointer-events-none absolute top-1 right-1.5 inline-flex size-5 items-center justify-center rounded-md text-icon-muted transition-opacity duration-150 max-sm:right-7 group-hover/project-header:opacity-0 group-focus-within/project-header:opacity-0 max-sm:group-hover/project-header:opacity-100 max-sm:group-focus-within/project-header:opacity-100"2357 className="pointer-events-none absolute top-1/2 right-1.5 inline-flex size-5 -translate-y-1/2 items-center justify-center rounded-md text-icon-muted transition-opacity duration-150 max-sm:right-7 group-hover/project-header:opacity-0 group-focus-within/project-header:opacity-0 max-sm:group-hover/project-header:opacity-100 max-sm:group-focus-within/project-header:opacity-100"
2355 />2358 />
2356 }2359 }
2357 >2360 >
2358 {project.allRemoteMembersAreDesktopLocal ? (2361 <EnvironmentMachineIcon kind={environmentMachine} className="size-3" />
2359 <ContainerIcon className="size-3" />
2360 ) : (
2361 <CloudIcon className="size-3" />
2362 )}
2363 </TooltipTrigger>2362 </TooltipTrigger>
2364 <TooltipPopup side="top">2363 <TooltipPopup side="top">
2365 {project.allRemoteMembersAreDesktopLocal2364 {project.allRemoteMembersAreDesktopLocal
...@@ -3132,6 +3131,15 @@ export default function LegacySidebar() {...@@ -3132,6 +3131,15 @@ export default function LegacySidebar() {
3132 ),3131 ),
3133 [environments],3132 [environments],
3134 );3133 );
3134 const wslEnvironmentIds = useMemo(
3135 () =>
3136 new Set(
3137 environments
3138 .filter((environment) => isWslConnectionTarget(environment.entry.target))
3139 .map((environment) => environment.environmentId),
3140 ),
3141 [environments],
3142 );
3135 const orderedProjects = useMemo(() => {3143 const orderedProjects = useMemo(() => {
3136 return orderItemsByPreferredIds({3144 return orderItemsByPreferredIds({
3137 items: projects,3145 items: projects,
...@@ -3172,10 +3180,12 @@ export default function LegacySidebar() {...@@ -3172,10 +3180,12 @@ export default function LegacySidebar() {
3172 primaryEnvironmentId,3180 primaryEnvironmentId,
3173 resolveEnvironmentLabel: (environmentId) => environmentLabelById.get(environmentId) ?? null,3181 resolveEnvironmentLabel: (environmentId) => environmentLabelById.get(environmentId) ?? null,
3174 isDesktopLocalEnvironment: (environmentId) => desktopLocalEnvironmentIds.has(environmentId),3182 isDesktopLocalEnvironment: (environmentId) => desktopLocalEnvironmentIds.has(environmentId),
3183 isWslEnvironment: (environmentId) => wslEnvironmentIds.has(environmentId),
3175 });3184 });
3176 }, [3185 }, [
3177 environmentLabelById,3186 environmentLabelById,
3178 desktopLocalEnvironmentIds,3187 desktopLocalEnvironmentIds,
3188 wslEnvironmentIds,
3179 orderedProjects,3189 orderedProjects,
3180 projectGroupingSettings,3190 projectGroupingSettings,
3181 primaryEnvironmentId,3191 primaryEnvironmentId,
apps/web/src/connection/desktopLocal.test.ts+13
...@@ -10,6 +10,7 @@ import {...@@ -10,6 +10,7 @@ import {
10 desktopLocalBackendId,10 desktopLocalBackendId,
11 desktopLocalConnectionId,11 desktopLocalConnectionId,
12 isDesktopLocalConnectionTarget,12 isDesktopLocalConnectionTarget,
13 isWslConnectionTarget,
13} from "./desktopLocal";14} from "./desktopLocal";
1415
15describe("desktop local connection identity", () => {16describe("desktop local connection identity", () => {
...@@ -22,6 +23,18 @@ describe("desktop local connection identity", () => {...@@ -22,6 +23,18 @@ describe("desktop local connection identity", () => {
2223
23 expect(isDesktopLocalConnectionTarget(target)).toBe(true);24 expect(isDesktopLocalConnectionTarget(target)).toBe(true);
24 expect(desktopLocalBackendId(target)).toBe("wsl:Ubuntu");25 expect(desktopLocalBackendId(target)).toBe("wsl:Ubuntu");
26 expect(isWslConnectionTarget(target)).toBe(true);
27 });
28
29 it("does not infer WSL for another desktop-local backend", () => {
30 const target = new BearerConnectionTarget({
31 connectionId: desktopLocalConnectionId("native-linux"),
32 environmentId: EnvironmentId.make("environment-native-linux"),
33 label: "Linux",
34 });
35
36 expect(isDesktopLocalConnectionTarget(target)).toBe(true);
37 expect(isWslConnectionTarget(target)).toBe(false);
25 });38 });
2639
27 it("does not classify the primary environment as desktop-local", () => {40 it("does not classify the primary environment as desktop-local", () => {
apps/web/src/connection/desktopLocal.ts+4
...@@ -38,6 +38,10 @@ export function desktopLocalBackendId(target: ConnectionTarget): string | null {...@@ -38,6 +38,10 @@ export function desktopLocalBackendId(target: ConnectionTarget): string | null {
38 : null;38 : null;
39}39}
4040
41export function isWslConnectionTarget(target: ConnectionTarget): boolean {
42 return desktopLocalBackendId(target)?.startsWith("wsl:") === true;
43}
44
41export type DesktopSecondaryBootstrapsRead =45export type DesktopSecondaryBootstrapsRead =
42 | {46 | {
43 readonly _tag: "Success";47 readonly _tag: "Success";
apps/web/src/sidebarProjectGrouping.ts+10-4
...@@ -15,11 +15,12 @@ export interface SidebarProjectSnapshot extends Project {...@@ -15,11 +15,12 @@ export interface SidebarProjectSnapshot extends Project {
15 groupedProjectCount: number;15 groupedProjectCount: number;
16 environmentPresence: EnvironmentPresence;16 environmentPresence: EnvironmentPresence;
17 // True iff every non-primary member of this group lives in a17 // True iff every non-primary member of this group lives in a
18 // desktopLocal env (today: the WSL backend). The sidebar uses this18 // desktop-local environment. The sidebar uses this
19 // to differentiate "lives on this machine but in a sandbox" from19 // to differentiate "lives on this machine but in a sandbox" from
20 // "lives on a real remote" so the project header can pick a20 // "lives on a real remote" so the project header can pick a
21 // container icon instead of the generic cloud icon.21 // local-device treatment instead of the generic remote treatment.
22 allRemoteMembersAreDesktopLocal: boolean;22 allRemoteMembersAreDesktopLocal: boolean;
23 allRemoteMembersAreWsl: boolean;
23 memberProjects: readonly SidebarProjectGroupMember[];24 memberProjects: readonly SidebarProjectGroupMember[];
24 memberProjectRefs: readonly ScopedProjectRef[];25 memberProjectRefs: readonly ScopedProjectRef[];
25 remoteEnvironmentLabels: readonly string[];26 remoteEnvironmentLabels: readonly string[];
...@@ -55,11 +56,12 @@ export function buildSidebarProjectSnapshots(input: {...@@ -55,11 +56,12 @@ export function buildSidebarProjectSnapshots(input: {
55 settings: ProjectGroupingSettings;56 settings: ProjectGroupingSettings;
56 primaryEnvironmentId: EnvironmentId | null;57 primaryEnvironmentId: EnvironmentId | null;
57 resolveEnvironmentLabel: (environmentId: EnvironmentId) => string | null;58 resolveEnvironmentLabel: (environmentId: EnvironmentId) => string | null;
58 // Returns true when an env id maps to a desktopLocal saved-env59 // Returns true when an env id maps to a desktop-local saved-env
59 // record (today: the WSL backend). Defaults to "false for every60 // record. Defaults to "false for every
60 // env" so callers that don't care about the distinction get the61 // env" so callers that don't care about the distinction get the
61 // legacy behavior.62 // legacy behavior.
62 isDesktopLocalEnvironment?: (environmentId: EnvironmentId) => boolean;63 isDesktopLocalEnvironment?: (environmentId: EnvironmentId) => boolean;
64 isWslEnvironment?: (environmentId: EnvironmentId) => boolean;
63}): SidebarProjectSnapshot[] {65}): SidebarProjectSnapshot[] {
64 return buildProjectGroups({66 return buildProjectGroups({
65 projects: input.projects,67 projects: input.projects,
...@@ -95,9 +97,12 @@ export function buildSidebarProjectSnapshots(input: {...@@ -95,9 +97,12 @@ export function buildSidebarProjectSnapshots(input: {
95 .flatMap((member) => (member.environmentLabel ? [member.environmentLabel] : []))97 .flatMap((member) => (member.environmentLabel ? [member.environmentLabel] : []))
96 .filter((label, index, labels) => labels.indexOf(label) === index);98 .filter((label, index, labels) => labels.indexOf(label) === index);
97 const isDesktopLocal = input.isDesktopLocalEnvironment ?? (() => false);99 const isDesktopLocal = input.isDesktopLocalEnvironment ?? (() => false);
100 const isWsl = input.isWslEnvironment ?? (() => false);
98 const allRemoteMembersAreDesktopLocal =101 const allRemoteMembersAreDesktopLocal =
99 remoteMembers.length > 0 &&102 remoteMembers.length > 0 &&
100 remoteMembers.every((member) => isDesktopLocal(member.environmentId));103 remoteMembers.every((member) => isDesktopLocal(member.environmentId));
104 const allRemoteMembersAreWsl =
105 remoteMembers.length > 0 && remoteMembers.every((member) => isWsl(member.environmentId));
101106
102 return {107 return {
103 ...representative,108 ...representative,
...@@ -107,6 +112,7 @@ export function buildSidebarProjectSnapshots(input: {...@@ -107,6 +112,7 @@ export function buildSidebarProjectSnapshots(input: {
107 environmentPresence:112 environmentPresence:
108 hasLocal && hasRemote ? "mixed" : hasRemote ? "remote-only" : "local-only",113 hasLocal && hasRemote ? "mixed" : hasRemote ? "remote-only" : "local-only",
109 allRemoteMembersAreDesktopLocal,114 allRemoteMembersAreDesktopLocal,
115 allRemoteMembersAreWsl,
110 memberProjects: members,116 memberProjects: members,
111 memberProjectRefs: group.memberProjectRefs,117 memberProjectRefs: group.memberProjectRefs,
112 remoteEnvironmentLabels,118 remoteEnvironmentLabels,
packages/contracts/src/environment.ts+2-1
...@@ -21,13 +21,14 @@ export const ExecutionEnvironmentPlatformArch = Schema.Literals(["arm64", "x64",...@@ -21,13 +21,14 @@ export const ExecutionEnvironmentPlatformArch = Schema.Literals(["arm64", "x64",
21export type ExecutionEnvironmentPlatformArch = typeof ExecutionEnvironmentPlatformArch.Type;21export type ExecutionEnvironmentPlatformArch = typeof ExecutionEnvironmentPlatformArch.Type;
2222
23/**23/**
24 * The curated set of machine shapes an environment can wear as its icon.24 * The curated set of machine shapes and OS identities an environment can wear as its icon.
25 * Servers detect one from the hardware they run on (`platform.machine`), and25 * Servers detect one from the hardware they run on (`platform.machine`), and
26 * the `environmentIcon` server setting lets a user pick one instead.26 * the `environmentIcon` server setting lets a user pick one instead.
27 */27 */
28export const ENVIRONMENT_MACHINE_KINDS = [28export const ENVIRONMENT_MACHINE_KINDS = [
29 "server",29 "server",
30 "cloud",30 "cloud",
31 "linux",
31 "desktop",32 "desktop",
32 "laptop",33 "laptop",
33 "mac-mini",34 "mac-mini",
packages/contracts/src/settings.test.ts+4
...@@ -625,6 +625,7 @@ describe("ServerSettings environment icon", () => {...@@ -625,6 +625,7 @@ describe("ServerSettings environment icon", () => {
625625
626 it("keeps a kind this build knows", () => {626 it("keeps a kind this build knows", () => {
627 expect(decodeServerSettings({ environmentIcon: "mac-mini" }).environmentIcon).toBe("mac-mini");627 expect(decodeServerSettings({ environmentIcon: "mac-mini" }).environmentIcon).toBe("mac-mini");
628 expect(decodeServerSettings({ environmentIcon: "linux" }).environmentIcon).toBe("linux");
628 });629 });
629630
630 it("decodes a kind from a newer server as null instead of failing the snapshot", () => {631 it("decodes a kind from a newer server as null instead of failing the snapshot", () => {
...@@ -634,5 +635,8 @@ describe("ServerSettings environment icon", () => {...@@ -634,5 +635,8 @@ describe("ServerSettings environment icon", () => {
634 it("round-trips through encode", () => {635 it("round-trips through encode", () => {
635 const settings = decodeServerSettings({ environmentIcon: "laptop" });636 const settings = decodeServerSettings({ environmentIcon: "laptop" });
636 expect(encodeServerSettings(settings).environmentIcon).toBe("laptop");637 expect(encodeServerSettings(settings).environmentIcon).toBe("laptop");
638
639 const linuxSettings = decodeServerSettings({ environmentIcon: "linux" });
640 expect(encodeServerSettings(linuxSettings).environmentIcon).toBe("linux");
637 });641 });
638});642});