Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions apps/web/src/components/EnvironmentOsIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { ExecutionEnvironmentPlatformOs } from "@t3tools/contracts";

import { cn } from "~/lib/utils";
import type { Icon } from "./Icons";
import { AppleIcon, LinuxIcon, WindowsIcon } from "./OsIcons";

/** Every OS the server can report, except "unknown", which has no glyph. */
const OS_PRESENTATION = {
darwin: { label: "macOS", Icon: AppleIcon },
linux: { label: "Linux", Icon: LinuxIcon },
windows: { label: "Windows", Icon: WindowsIcon },
} as const satisfies Record<
Exclude<ExecutionEnvironmentPlatformOs, "unknown">,
{ readonly label: string; readonly Icon: Icon }
>;

/**
* The platform glyph beside a remote environment's name. `os` is null when no
* descriptor is available (an environment that has never connected and is
* currently unreachable), in which case nothing renders — an absent glyph
* reads better than a placeholder for an unknown platform.
*/
export function EnvironmentOsIcon({
os,
className,
}: {
readonly os: ExecutionEnvironmentPlatformOs | null;
readonly className?: string;
}) {
if (os === null || os === "unknown") {
return null;
}
const { label, Icon } = OS_PRESENTATION[os];

return (
<Icon
role="img"
aria-label={label}
className={cn("size-3.5 shrink-0 text-muted-foreground", className)}
/>
);
}
57 changes: 57 additions & 0 deletions apps/web/src/components/OsIcons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Icon } from "./Icons";

/**
* Monochrome OS logos for the remote-environment list. Every mark is drawn in
* `currentColor` so it inherits the row's text colour and works in both themes.
*
* Apple (svgl) ships fill-less, which would render black on a dark background,
* so the fill is pinned to currentColor and a `stroke-foreground/70` rim separates
* the silhouette from the surface behind it — that token stays higher-contrast
* than the muted fill in both themes, so the rim reads light on dark and dark
* on light rather than disappearing. Windows (svgl) deliberately has no
* rim: its four panes are split by thin gutters that a stroke closes up at the
* 14px size these render at, turning the glyph into a solid block.
*
* Tux is Simple Icons' single-path mark rather than svgl's, whose full-colour
* artwork defines the eyes and beak purely through colour contrast and so
* collapses into an unreadable blob when flattened to one fill. The body is
* filled with currentColor and the face is drawn over it in a fixed dark tone.
* That tone is deliberately not the surface colour: these rows also render on
* the raised popover of the T3 Connect onboarding dialog, and painting the face
* with `background` there would leave a mismatched patch instead of a cutout.
* It only has to stay darker than the muted body, which holds in both themes.
*/

export const AppleIcon: Icon = (props) => (
<svg {...props} viewBox="0 0 814 1000">
<path
fill="currentColor"
className="stroke-foreground/70"
strokeWidth="16"
strokeLinejoin="round"
d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"
/>
</svg>
);

export const WindowsIcon: Icon = (props) => (
<svg {...props} viewBox="0 0 88 88">
<path
fill="currentColor"
d="m0 12.402 35.687-4.86.016 34.423-35.67.203zm35.67 33.529.028 34.453L.028 75.48.026 45.7zm4.326-39.025L87.314 0v41.527l-47.318.376zm47.329 39.349-.011 41.34-47.318-6.678-.066-34.739z"
/>
</svg>
);

export const LinuxIcon: Icon = (props) => (
<svg {...props} viewBox="0 0 24 24">
<path
fill="currentColor"
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.139z"
/>
<path
className="fill-zinc-900"
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"
/>
</svg>
);
Comment thread
cursor[bot] marked this conversation as resolved.
85 changes: 85 additions & 0 deletions apps/web/src/components/cloud/CloudEnvironmentConnectList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { type EnvironmentConnectionPresentation } from "@t3tools/client-runtime/connection";
import { type Discovery } from "@t3tools/client-runtime/relay";
import {
EnvironmentId,
type ExecutionEnvironmentDescriptor,
type ServerConfig,
} from "@t3tools/contracts";
import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay";
import * as Option from "effect/Option";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it, vi } from "vite-plus/test";

const testState = vi.hoisted(() => ({
discovery: null as Discovery.RelayEnvironmentDiscoveryState | null,
}));

vi.mock("~/connection/catalog", () => ({
environmentCatalog: { register: Symbol("register") },
}));

vi.mock("~/state/relay", () => ({
relayEnvironmentDiscovery: { refresh: Symbol("refresh") },
}));

vi.mock("~/state/environments", () => ({
useRelayEnvironmentDiscovery: () => testState.discovery,
}));

vi.mock("~/state/use-atom-command", () => ({
useAtomCommand: () => vi.fn(),
}));

import { CloudEnvironmentConnectRows } from "./CloudEnvironmentConnectList";

const environmentId = EnvironmentId.make("saved-windows-environment");
const descriptor = {
platform: { os: "windows", arch: "x64" },
} as ExecutionEnvironmentDescriptor;
const serverConfig = { environment: descriptor } as ServerConfig;
const connection: EnvironmentConnectionPresentation = {
phase: "offline",
error: null,
traceId: null,
};
const relayEnvironment: RelayClientEnvironmentRecord = {
environmentId,
label: "Saved Windows environment",
endpoint: {
httpBaseUrl: "https://saved-windows.example.test",
wsBaseUrl: "wss://saved-windows.example.test",
providerKind: "t3_relay",
},
linkedAt: "2026-08-08T12:00:00.000Z",
};

describe("CloudEnvironmentConnectRows", () => {
it("keeps the cached OS glyph when a saved environment's relay is offline", () => {
testState.discovery = {
environments: new Map([
[
environmentId,
{
environment: relayEnvironment,
availability: "offline",
status: Option.none(),
error: Option.none(),
},
],
]),
refreshing: false,
offline: false,
error: Option.none(),
};

const markup = renderToStaticMarkup(
<CloudEnvironmentConnectRows
primaryEnvironmentId={null}
savedEnvironments={[{ environmentId, connection, serverConfig }]}
showSavedEnvironments
/>,
);

expect(markup).toContain('aria-label="Windows"');
});
});
13 changes: 11 additions & 2 deletions apps/web/src/components/cloud/CloudEnvironmentConnectList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
isAtomCommandInterrupted,
squashAtomCommandFailure,
} from "@t3tools/client-runtime/state/runtime";
import type { EnvironmentId } from "@t3tools/contracts";
import type { EnvironmentId, ServerConfig } from "@t3tools/contracts";
import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay";
import * as Option from "effect/Option";
import { type ReactNode, useCallback, useEffect, useState } from "react";
Expand All @@ -19,6 +19,8 @@ import { relayEnvironmentDiscovery } from "~/state/relay";
import { useRelayEnvironmentDiscovery } from "~/state/environments";
import { useAtomCommand } from "~/state/use-atom-command";
import { ConnectionStatusDot } from "../ConnectionStatusDot";
import { EnvironmentOsIcon } from "../EnvironmentOsIcon";
import { resolveEnvironmentOs } from "../settings/ConnectionsSettings.logic";
import { ITEM_ROW_CLASSNAME, ITEM_ROW_INNER_CLASSNAME } from "../settings/itemRows";
import { Button } from "../ui/button";
import { Skeleton } from "../ui/skeleton";
Expand All @@ -28,6 +30,7 @@ import { presentSavedCloudEnvironmentConnection } from "./cloudEnvironmentConnec
export interface SavedCloudEnvironmentConnection {
readonly environmentId: EnvironmentId;
readonly connection: EnvironmentConnectionPresentation;
readonly serverConfig: ServerConfig | null;
}

export function RemoteEnvironmentRowsSkeleton() {
Expand Down Expand Up @@ -171,7 +174,7 @@ export function CloudEnvironmentConnectRows({
return empty;
}

return visibleEnvironments.map(({ environment, availability, error }) => {
return visibleEnvironments.map(({ environment, availability, status, error }) => {
const savedEnvironment = savedById.get(environment.environmentId);
const savedConnection = savedEnvironment
? presentSavedCloudEnvironmentConnection(savedEnvironment.connection)
Expand Down Expand Up @@ -226,6 +229,12 @@ export function CloudEnvironmentConnectRows({
}
/>
<p className="truncate text-sm font-medium">{environment.label}</p>
<EnvironmentOsIcon
os={resolveEnvironmentOs({
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
serverConfig: savedEnvironment?.serverConfig ?? null,
discoveredDescriptor: Option.getOrNull(status)?.descriptor ?? null,
})}
/>
Comment thread
cursor[bot] marked this conversation as resolved.
</div>
<p
className={cn(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import type { AdvertisedEndpoint, DesktopWslState } from "@t3tools/contracts";
import type {
AdvertisedEndpoint,
DesktopWslState,
ExecutionEnvironmentDescriptor,
ExecutionEnvironmentPlatformOs,
ServerConfig,
} from "@t3tools/contracts";
import { describe, expect, it, vi } from "vite-plus/test";
import {
applyWslEnableSelection,
isQrShareableEndpoint,
resolveEnvironmentOs,
selectQrEndpointOption,
} from "./ConnectionsSettings.logic";

Expand Down Expand Up @@ -163,3 +170,34 @@ describe("selectQrEndpointOption", () => {
expect(selectQrEndpointOption([], "anything", "anything")).toBeNull();
});
});

describe("resolveEnvironmentOs", () => {
const descriptorWith = (os: ExecutionEnvironmentPlatformOs) =>
({ platform: { os, arch: "arm64" } }) as ExecutionEnvironmentDescriptor;
const serverConfigWith = (os: ExecutionEnvironmentPlatformOs) =>
({ environment: descriptorWith(os) }) as ServerConfig;

it("prefers the cached server config so a disconnected environment keeps its glyph", () => {
expect(
resolveEnvironmentOs({
serverConfig: serverConfigWith("darwin"),
discoveredDescriptor: descriptorWith("linux"),
}),
).toBe("darwin");
});

it("falls back to relay discovery for an environment this client never connected to", () => {
expect(
resolveEnvironmentOs({ serverConfig: null, discoveredDescriptor: descriptorWith("windows") }),
).toBe("windows");
});

it("returns null when neither source knows the platform", () => {
expect(resolveEnvironmentOs({})).toBeNull();
expect(resolveEnvironmentOs({ serverConfig: null, discoveredDescriptor: null })).toBeNull();
});

it("treats a reported 'unknown' as no glyph rather than a placeholder", () => {
expect(resolveEnvironmentOs({ serverConfig: serverConfigWith("unknown") })).toBeNull();
});
});
31 changes: 30 additions & 1 deletion apps/web/src/components/settings/ConnectionsSettings.logic.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,36 @@
import type { AdvertisedEndpoint, DesktopBridge, DesktopWslState } from "@t3tools/contracts";
import type {
AdvertisedEndpoint,
DesktopBridge,
DesktopWslState,
ExecutionEnvironmentDescriptor,
ExecutionEnvironmentPlatformOs,
ServerConfig,
} from "@t3tools/contracts";

type WslEnableBridge = Pick<DesktopBridge, "setWslBackendEnabled" | "setWslDistro" | "setWslOnly">;

/**
* Which OS a remote environment runs on, for the platform glyph in the
* environment list. The server config is authoritative and survives
* disconnects (it is cached per environment), so a previously-connected
* environment keeps its glyph while offline. Relay discovery only reports a
* descriptor while the environment is reachable, so it is the fallback for
* environments this client has never connected to.
*
* Returns null when neither source knows the platform, or when the server
* reported "unknown" — callers render no glyph rather than a placeholder.
*/
export function resolveEnvironmentOs(input: {
readonly serverConfig?: ServerConfig | null;
readonly discoveredDescriptor?: ExecutionEnvironmentDescriptor | null;
}): ExecutionEnvironmentPlatformOs | null {
const os =
input.serverConfig?.environment.platform.os ??
input.discoveredDescriptor?.platform.os ??
"unknown";
return os === "unknown" ? null : os;
}

/**
* A QR code encoding a loopback URL makes the scanning device dial itself, so
* loopback endpoints stay copyable from the endpoint menu but are never
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/components/settings/ConnectionsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { resolveDesktopPairingUrl, resolveHostedPairingUrl } from "./pairingUrls
import {
applyWslEnableSelection,
isQrShareableEndpoint,
resolveEnvironmentOs,
selectQrEndpointOption,
} from "./ConnectionsSettings.logic";
import {
Expand Down Expand Up @@ -127,6 +128,7 @@ import {
import { useAtomCommand } from "../../state/use-atom-command";
import { serverEnvironment } from "~/state/server";
import { ConnectionStatusDot } from "../ConnectionStatusDot";
import { EnvironmentOsIcon } from "../EnvironmentOsIcon";
import { ServerUpdateAction, ServerUpdateProgress } from "../ServerUpdateAction";
import { CloudEnvironmentConnectRows } from "../cloud/CloudEnvironmentConnectList";
import { ITEM_ROW_CLASSNAME, ITEM_ROW_INNER_CLASSNAME } from "./itemRows";
Expand Down Expand Up @@ -1419,6 +1421,9 @@ function SavedBackendListRow({
}
/>
<h3 className="text-sm font-medium text-foreground">{environment.label}</h3>
<EnvironmentOsIcon
os={resolveEnvironmentOs({ serverConfig: environment.serverConfig })}
/>
</div>
{metadataBits.length > 0 ? (
<p className="text-xs text-muted-foreground">{metadataBits.join(" · ")}</p>
Expand Down
Loading