authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-02 23:23:44-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-02 23:23:44-07:00
log829c3db94830fc70b5754513b8a370f7301ca213
treec06233d52836383e03d9ae02a55f6f65a8500212
parent5eb4f452ee0fcde7b5bbc93d62de9118b70a33b2
signature Signed by PGP key B5690EEEBB952194

fix(environments): draw the machine icon everywhere an environment is named (#9365)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

10 files changed, 130 insertions(+), 70 deletions(-)

apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx+29-6
......@@ -3,7 +3,11 @@ import type {
33 EnvironmentThreadShell,
44} from "@t3tools/client-runtime/state/shell";
55import { LegendList } from "@legendapp/list/react-native";
6import type { EnvironmentId } from "@t3tools/contracts";
6import {
7 type EnvironmentId,
8 type EnvironmentMachineKind,
9 resolveEnvironmentMachineKind,
10} from "@t3tools/contracts";
711import type { MenuAction } from "@react-native-menu/menu";
812import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
913import { SymbolView } from "../../components/AppSymbol";
......@@ -24,10 +28,12 @@ import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSw
2428import { AppText as Text } from "../../components/AppText";
2529import { ControlPillMenu } from "../../components/ControlPill";
2630import { EmptyState } from "../../components/EmptyState";
31import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol";
2732import { ProjectFavicon } from "../../components/ProjectFavicon";
2833import { useSafeAreaInsets } from "react-native-safe-area-context";
2934import { relativeTime } from "../../lib/time";
3035import { useUniwindTheme } from "../../lib/useUniwindTheme";
36import { useServerConfigs } from "../../state/entities";
3137import { ThreadSwipeable } from "../home/thread-swipe-actions";
3238import {
3339 createNativeMailSearchToolbarItem,
......@@ -45,6 +51,7 @@ type ArchivedThreadListItem =
4551 readonly kind: "project";
4652 readonly key: string;
4753 readonly environmentLabel: string | null;
54 readonly environmentMachine: EnvironmentMachineKind;
4855 readonly project: EnvironmentProject;
4956 }
5057 | {
......@@ -360,6 +367,7 @@ function ArchivedThreadsHeader(props: {
360367
361368function ProjectGroupLabel(props: {
362369 readonly environmentLabel: string | null;
370 readonly environmentMachine: EnvironmentMachineKind;
363371 readonly project: EnvironmentProject;
364372}) {
365373 return (
......@@ -378,9 +386,16 @@ function ProjectGroupLabel(props: {
378386 {props.project.title}
379387 </Text>
380388 {props.environmentLabel ? (
381 <Text className="max-w-[42%] text-2xs text-foreground-tertiary" numberOfLines={1}>
382 {props.environmentLabel}
383 </Text>
389 <View className="max-w-[42%] flex-row items-center gap-1">
390 <EnvironmentMachineSymbol
391 kind={props.environmentMachine}
392 size={10}
393 tintColorClassName="accent-foreground-tertiary"
394 />
395 <Text className="shrink text-2xs text-foreground-tertiary" numberOfLines={1}>
396 {props.environmentLabel}
397 </Text>
398 </View>
384399 ) : null}
385400 </View>
386401 );
......@@ -517,6 +532,7 @@ export function ArchivedThreadsScreen(props: {
517532 ),
518533 [props.environments],
519534 );
535 const serverConfigs = useServerConfigs();
520536 const listItems = useMemo<ReadonlyArray<ArchivedThreadListItem>>(() => {
521537 const items: ArchivedThreadListItem[] = [];
522538 for (const group of props.groups) {
......@@ -525,6 +541,9 @@ export function ArchivedThreadsScreen(props: {
525541 kind: "project",
526542 key: `${group.key}:project`,
527543 environmentLabel,
544 environmentMachine: resolveEnvironmentMachineKind(
545 serverConfigs.get(group.project.environmentId) ?? null,
546 ),
528547 project: group.project,
529548 });
530549
......@@ -540,7 +559,7 @@ export function ArchivedThreadsScreen(props: {
540559 });
541560 }
542561 return items;
543 }, [environmentLabelsById, props.groups]);
562 }, [environmentLabelsById, props.groups, serverConfigs]);
544563 const handleSwipeableWillOpen = useCallback((methods: SwipeableMethods) => {
545564 if (openSwipeableRef.current && openSwipeableRef.current !== methods) {
546565 openSwipeableRef.current.close();
......@@ -559,7 +578,11 @@ export function ArchivedThreadsScreen(props: {
559578 if (item.kind === "project") {
560579 return (
561580 <View className="pt-4">
562 <ProjectGroupLabel environmentLabel={item.environmentLabel} project={item.project} />
581 <ProjectGroupLabel
582 environmentLabel={item.environmentLabel}
583 environmentMachine={item.environmentMachine}
584 project={item.project}
585 />
563586 </View>
564587 );
565588 }
apps/mobile/src/features/projects/AddProjectScreen.tsx+13-5
......@@ -31,7 +31,13 @@ import {
3131 inferProjectTitleFromPath,
3232 isWindowsPlatform,
3333} from "@t3tools/client-runtime/state/projects";
34import { CommandId, type EnvironmentId, ProjectId } from "@t3tools/contracts";
34import {
35 CommandId,
36 type EnvironmentId,
37 type EnvironmentMachineKind,
38 ProjectId,
39 resolveEnvironmentMachineKind,
40} from "@t3tools/contracts";
3541import { CommonActions, StackActions, useNavigation } from "@react-navigation/native";
3642import { SymbolView } from "../../components/AppSymbol";
3743import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
......@@ -49,6 +55,7 @@ import { projectEnvironment } from "../../state/projects";
4955import { useEnvironmentQuery } from "../../state/query";
5056import { sourceControlEnvironment } from "../../state/sourceControl";
5157import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
58import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol";
5259import { ErrorBanner } from "../../components/ErrorBanner";
5360import { SourceControlIcon } from "../../components/SourceControlIcon";
5461import { uuidv4 } from "../../lib/uuid";
......@@ -65,6 +72,7 @@ interface EnvironmentOption {
6572 readonly environmentId: EnvironmentId;
6673 readonly label: string;
6774 readonly platform: string;
75 readonly machine: EnvironmentMachineKind;
6876 readonly baseDirectory: string | null;
6977 readonly connectionState: EnvironmentConnectionPhase;
7078 readonly connectionError: string | null;
......@@ -352,6 +360,7 @@ function useEnvironmentOptions(): ReadonlyArray<EnvironmentOption> {
352360 environmentId: connection.environmentId,
353361 label: connection.environmentLabel,
354362 platform: platformFromOs(config?.environment.platform.os ?? null),
363 machine: resolveEnvironmentMachineKind(config ?? null),
355364 baseDirectory: config?.settings.addProjectBaseDirectory ?? null,
356365 connectionState: runtime?.connectionState ?? "available",
357366 connectionError: runtime?.connectionError ?? null,
......@@ -490,11 +499,10 @@ export function AddProjectSourceScreen() {
490499 })
491500 }
492501 icon={
493 <SymbolView
494 name="server.rack"
502 <EnvironmentMachineSymbol
503 kind={environment.machine}
495504 size={17}
496 tintColorClassName={"accent-icon"}
497 type="monochrome"
505 tintColorClassName="accent-icon"
498506 />
499507 }
500508 selected={environment.environmentId === selectedEnvironment?.environmentId}
apps/mobile/src/features/settings/SettingsClientStorageRouteScreen.tsx+9-7
......@@ -1,4 +1,5 @@
11import { useAtomSet, useAtomValue } from "@effect/atom-react";
2import { type EnvironmentMachineKind, resolveEnvironmentMachineKind } from "@t3tools/contracts";
23import { AsyncResult } from "effect/unstable/reactivity";
34import { useMemo } from "react";
45import { ActivityIndicator, Alert, Pressable, ScrollView, View } from "react-native";
......@@ -6,11 +7,13 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
67
78import { AppText as Text } from "../../components/AppText";
89import { SymbolView } from "../../components/AppSymbol";
10import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol";
911import {
1012 clearClientCacheAtom,
1113 clientCacheSummaryAtom,
1214 type EnvironmentClientCacheSummary,
1315} from "../../state/client-cache-state";
16import { useServerConfigs } from "../../state/entities";
1417import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry";
1518import { SettingsSection } from "./components/SettingsSection";
1619
......@@ -20,6 +23,7 @@ export function SettingsClientStorageRouteScreen() {
2023 const clearResult = useAtomValue(clearClientCacheAtom);
2124 const clearCache = useAtomSet(clearClientCacheAtom);
2225 const { savedConnectionsById } = useSavedRemoteConnections();
26 const serverConfigs = useServerConfigs();
2327 const isClearing = clearResult.waiting;
2428 const summary = AsyncResult.isSuccess(summaryResult) ? summaryResult.value : null;
2529 const environmentSummaries = useMemo(
......@@ -106,6 +110,9 @@ export function SettingsClientStorageRouteScreen() {
106110 savedConnectionsById[environment.environmentId]?.environmentLabel ??
107111 environment.environmentId
108112 }
113 machine={resolveEnvironmentMachineKind(
114 serverConfigs.get(environment.environmentId) ?? null,
115 )}
109116 disabled={isClearing}
110117 first={index === 0}
111118 onClear={() => confirmClearEnvironment(environment)}
......@@ -169,6 +176,7 @@ export function SettingsClientStorageRouteScreen() {
169176function CacheEnvironmentRow(props: {
170177 readonly environment: EnvironmentClientCacheSummary;
171178 readonly environmentLabel: string;
179 readonly machine: EnvironmentMachineKind;
172180 readonly disabled: boolean;
173181 readonly first: boolean;
174182 readonly onClear: () => void;
......@@ -181,13 +189,7 @@ function CacheEnvironmentRow(props: {
181189 : "border-t border-border flex-row items-center gap-3 p-4"
182190 }
183191 >
184 <SymbolView
185 name="desktopcomputer"
186 size={22}
187 tintColorClassName={"accent-icon"}
188 type="monochrome"
189 weight="regular"
190 />
192 <EnvironmentMachineSymbol kind={props.machine} size={22} tintColorClassName="accent-icon" />
191193 <Text className="min-w-0 flex-1 text-base text-foreground" numberOfLines={1}>
192194 {props.environmentLabel}
193195 </Text>
apps/mobile/src/features/threads/NewTaskContextPickerScreens.tsx+19-5
......@@ -1,4 +1,5 @@
11import type { VcsRef } from "@t3tools/client-runtime/state/vcs";
2import { resolveEnvironmentMachineKind } from "@t3tools/contracts";
23import { LegendList } from "@legendapp/list/react-native";
34import {
45 isAtomCommandInterrupted,
......@@ -21,9 +22,11 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
2122import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
2223import { SymbolView } from "../../components/AppSymbol";
2324import { AppText as Text } from "../../components/AppText";
25import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol";
2426import { ThemedSwitch } from "../../components/ThemedSwitch";
2527import { cn } from "../../lib/cn";
2628import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
29import { useServerConfigs } from "../../state/entities";
2730import { useAtomCommand } from "../../state/use-atom-command";
2831import { vcsEnvironment } from "../../state/vcs";
2932import {
......@@ -35,7 +38,7 @@ import { branchBadgeLabel, useNewTaskFlow } from "./new-task-flow-provider";
3538import { shouldCheckoutNewTaskBranch } from "./new-task-context-presentation";
3639
3740function SelectionRow(props: {
38 readonly icon?: "arrow.triangle.branch" | "desktopcomputer";
41 readonly icon?: "arrow.triangle.branch" | ReactNode;
3942 readonly onPress: () => void;
4043 readonly disabled?: boolean;
4144 readonly selected: boolean;
......@@ -56,14 +59,16 @@ function SelectionRow(props: {
5659 onPress={props.onPress}
5760 style={{ opacity: props.disabled ? 0.45 : 1 }}
5861 >
59 {props.icon ? (
62 {props.icon === "arrow.triangle.branch" ? (
6063 <SymbolView
61 name={props.icon}
64 name="arrow.triangle.branch"
6265 size={17}
6366 tintColorClassName={"accent-icon-muted"}
6467 type="monochrome"
6568 />
66 ) : null}
69 ) : (
70 (props.icon ?? null)
71 )}
6772 <View className="min-w-0 flex-1 gap-0.5">
6873 <Text className="text-base font-t3-medium text-foreground" numberOfLines={1}>
6974 {props.title}
......@@ -145,6 +150,7 @@ export function NewTaskEnvironmentPickerRouteScreen() {
145150 const flow = useNewTaskFlow();
146151 const navigation = useNavigation();
147152 const insets = useSafeAreaInsets();
153 const serverConfigs = useServerConfigs();
148154
149155 return (
150156 <View className="flex-1 bg-sheet" collapsable={false}>
......@@ -170,7 +176,15 @@ export function NewTaskEnvironmentPickerRouteScreen() {
170176 {flow.environments.map((environment, index) => (
171177 <SelectionRow
172178 key={String(environment.environmentId)}
173 icon="desktopcomputer"
179 icon={
180 <EnvironmentMachineSymbol
181 kind={resolveEnvironmentMachineKind(
182 serverConfigs.get(environment.environmentId) ?? null,
183 )}
184 size={17}
185 tintColorClassName="accent-icon-muted"
186 />
187 }
174188 isLast={index === flow.environments.length - 1}
175189 onPress={() => {
176190 void Haptics.selectionAsync();
apps/mobile/src/features/threads/NewTaskDraftScreen.tsx+12-2
......@@ -24,7 +24,10 @@ import {
2424 isAtomCommandInterrupted,
2525 squashAtomCommandFailure,
2626} from "@t3tools/client-runtime/state/runtime";
27import { PROVIDER_SEND_TURN_MAX_ATTACHMENTS } from "@t3tools/contracts";
27import {
28 PROVIDER_SEND_TURN_MAX_ATTACHMENTS,
29 resolveEnvironmentMachineKind,
30} from "@t3tools/contracts";
2831
2932import { ComposerEditor, type ComposerEditorHandle } from "../../components/ComposerEditor";
3033import {
......@@ -36,6 +39,7 @@ import {
3639import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
3740import { ComposerAttachmentButton } from "../../components/ComposerAttachmentButton";
3841import { ComposerAttachmentStrip } from "../../components/ComposerAttachmentStrip";
42import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol";
3943import {
4044 composerAttachmentUploadBlockReason,
4145 composerAttachmentUploadsAtom,
......@@ -1129,7 +1133,13 @@ export function NewTaskDraftScreen(props: {
11291133 accessibilityLabel={`Environment: ${selectedEnvironmentLabel}`}
11301134 chevronDirection="right"
11311135 disabled={isComposerInteractionLocked || voiceInput.isBusy}
1132 icon="desktopcomputer"
1136 iconNode={
1137 <EnvironmentMachineSymbol
1138 kind={resolveEnvironmentMachineKind(selectedEnvironmentServerConfig)}
1139 size={16}
1140 tintColorClassName="accent-icon-muted"
1141 />
1142 }
11331143 label={`on ${selectedEnvironmentLabel}`}
11341144 maxWidth={260}
11351145 onPress={
apps/web/src/components/BranchToolbarEnvironmentSelector.tsx+10-16
......@@ -1,8 +1,8 @@
11import type { EnvironmentId } from "@t3tools/contracts";
2import { CloudIcon, MonitorIcon } from "lucide-react";
32import { memo, useMemo } from "react";
43
54import type { EnvironmentOption } from "./BranchToolbar.logic";
5import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";
66import {
77 Select,
88 SelectGroup,
......@@ -52,11 +52,10 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir
5252 className="inline-flex h-7 min-w-0 max-w-full items-center gap-1 border border-transparent px-[calc(--spacing(3)-1px)] text-sm font-medium text-muted-foreground/70 sm:h-6 sm:text-xs"
5353 data-composer-context-control
5454 >
55 {activeEnvironment?.isPrimary ? (
56 <MonitorIcon className="size-3 shrink-0" />
57 ) : (
58 <CloudIcon className="size-3 shrink-0" />
59 )}
55 <EnvironmentMachineIcon
56 kind={activeEnvironment?.machine ?? "server"}
57 className="size-3 shrink-0"
58 />
6059 <span
6160 data-composer-label
6261 className="min-w-0 max-w-[240px] group-data-[compact]/composer-context:max-w-0"
......@@ -86,11 +85,10 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir
8685 aria-label="Run on"
8786 data-composer-context-control
8887 >
89 {activeEnvironment?.isPrimary ? (
90 <MonitorIcon className="size-3 shrink-0" />
91 ) : (
92 <CloudIcon className="size-3 shrink-0" />
93 )}
88 <EnvironmentMachineIcon
89 kind={activeEnvironment?.machine ?? "server"}
90 className="size-3 shrink-0"
91 />
9492 <span
9593 data-composer-label
9694 className="min-w-0 max-w-[240px] group-data-[compact]/composer-context:max-w-0"
......@@ -109,11 +107,7 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir
109107 {availableEnvironments.map((env) => (
110108 <SelectItem key={env.environmentId} value={env.environmentId}>
111109 <span className="inline-flex items-center gap-1.5">
112 {env.isPrimary ? (
113 <MonitorIcon className="size-3" />
114 ) : (
115 <CloudIcon className="size-3" />
116 )}
110 <EnvironmentMachineIcon kind={env.machine} className="size-3" />
117111 {env.label}
118112 </span>
119113 </SelectItem>
apps/web/src/components/CommandPalette.tsx+9-2
......@@ -36,6 +36,7 @@ import {
3636 type SourceControlProviderKind,
3737 type SourceControlRepositoryInfo,
3838 PRIMARY_LOCAL_ENVIRONMENT_ID,
39 resolveEnvironmentMachineKind,
3940} from "@t3tools/contracts";
4041import { useLocation, useNavigate, useParams } from "@tanstack/react-router";
4142import * as Option from "effect/Option";
......@@ -48,7 +49,6 @@ import {
4849 LinkIcon,
4950 MessageSquareIcon,
5051 PaletteIcon,
51 ServerIcon,
5252 SettingsIcon,
5353 SquarePenIcon,
5454 TextSearchIcon,
......@@ -145,6 +145,7 @@ import { resolveEnvironmentOptionLabel } from "./BranchToolbar.logic";
145145import { CommandPaletteContent } from "./CommandPaletteContent";
146146import { CommandPaletteResults } from "./CommandPaletteResults";
147147import { AzureDevOpsIcon, BitbucketIcon, GitHubIcon, GitLabIcon } from "./Icons";
148import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";
148149import { ProjectFavicon } from "./ProjectFavicon";
149150import { ProjectFilePicker } from "./files/ProjectFilePicker";
150151import { ProjectContentSearchDialog } from "./search/ProjectContentSearchDialog";
......@@ -738,6 +739,7 @@ function OpenCommandPaletteDialog(props: {
738739 : isLocal
739740 ? `${environment.label} (Local)`
740741 : environment.label,
742 machine: resolveEnvironmentMachineKind(environment.serverConfig),
741743 },
742744 ] as const;
743745 }),
......@@ -1119,12 +1121,17 @@ function OpenCommandPaletteDialog(props: {
11191121 const location = projectEnvironmentLocationById.get(project.environmentId) ?? {
11201122 kind: "remote",
11211123 label: "Remote",
1124 machine: "server" as const,
11221125 };
11231126 return (
11241127 <span className="flex min-w-0 items-center gap-1">
11251128 <span className="inline-flex min-w-0 items-center gap-1">
11261129 {location.kind === "remote" ? (
1127 <ServerIcon aria-hidden className={COMMAND_PALETTE_META_ICON_CLASS} />
1130 <EnvironmentMachineIcon
1131 aria-hidden
1132 kind={location.machine}
1133 className={COMMAND_PALETTE_META_ICON_CLASS}
1134 />
11281135 ) : null}
11291136 <span className="truncate">{location.label}</span>
11301137 </span>
apps/web/src/components/ThreadStatusIndicators.tsx+16-5
......@@ -4,12 +4,18 @@ import {
44 scopeThreadRef,
55} from "@t3tools/client-runtime/environment";
66import { pullRequestDetailToVcsStatus } from "@t3tools/client-runtime/state/pull-requests";
7import type { EnvironmentId, ThreadLinkedPullRequest, VcsStatusResult } from "@t3tools/contracts";
7import {
8 type EnvironmentId,
9 resolveEnvironmentMachineKind,
10 type ThreadLinkedPullRequest,
11 type VcsStatusResult,
12} from "@t3tools/contracts";
813import { Atom } from "effect/unstable/reactivity";
9import { CloudIcon, FolderGit2Icon, GitPullRequestIcon, TerminalIcon } from "lucide-react";
14import { FolderGit2Icon, GitPullRequestIcon, TerminalIcon } from "lucide-react";
1015import { useMemo } from "react";
1116import { appAtomRegistry } from "../rpc/atomRegistry";
1217import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments";
18import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";
1319import { useProject } from "../state/entities";
1420import { useEnvironmentQuery } from "../state/query";
1521import { linkedPullRequestDetailAtom, useSharedPullRequestSummary } from "../state/pullRequests";
......@@ -602,10 +608,12 @@ export function ThreadRowTrailingStatus({ thread }: { thread: SidebarThreadSumma
602608 });
603609 const environment = useEnvironment(thread.environmentId);
604610 const primaryEnvironmentId = usePrimaryEnvironmentId();
605 const isRemoteThread =
606 primaryEnvironmentId !== null && thread.environmentId !== primaryEnvironmentId;
611 // No primary (the hosted app) means every thread is remote, and the machine
612 // glyph is what tells the environments apart.
613 const isRemoteThread = thread.environmentId !== primaryEnvironmentId;
607614 const remoteEnvLabel = environment?.label ?? null;
608615 const threadEnvironmentLabel = isRemoteThread ? (remoteEnvLabel ?? "Remote") : null;
616 const remoteMachine = resolveEnvironmentMachineKind(environment?.serverConfig ?? null);
609617 const terminalStatus = terminalStatusFromRunningIds(runningTerminalIds);
610618
611619 if (!terminalStatus && !isRemoteThread) {
......@@ -642,7 +650,10 @@ export function ThreadRowTrailingStatus({ thread }: { thread: SidebarThreadSumma
642650 />
643651 }
644652 >
645 <CloudIcon className="size-3 text-muted-foreground/60" />
653 <EnvironmentMachineIcon
654 kind={remoteMachine}
655 className="size-3 text-muted-foreground/60"
656 />
646657 </TooltipTrigger>
647658 <TooltipPopup side="top">{threadEnvironmentLabel}</TooltipPopup>
648659 </Tooltip>
apps/web/src/components/settings/ProviderSettingsPanel.tsx+9-20
......@@ -12,6 +12,7 @@ import {
1212 ProviderDriverKind,
1313 type ProviderInstanceConfig,
1414 type ProviderInstanceId,
15 resolveEnvironmentMachineKind,
1516 resolveProviderInstanceEnabled,
1617} from "@t3tools/contracts";
1718import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";
......@@ -23,22 +24,14 @@ import * as Arr from "effect/Array";
2324import * as Duration from "effect/Duration";
2425import * as Equal from "effect/Equal";
2526import * as Result from "effect/Result";
26import {
27 ChevronDownIcon,
28 CloudIcon,
29 LaptopIcon,
30 LoaderIcon,
31 MonitorIcon,
32 PlusIcon,
33 RefreshCwIcon,
34 TerminalIcon,
35} from "lucide-react";
27import { ChevronDownIcon, LoaderIcon, PlusIcon, RefreshCwIcon } from "lucide-react";
3628import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
3729
3830import { isDesktopLocalConnectionTarget } from "../../connection/desktopLocal";
3931import { isElectron } from "../../env";
4032import { usePrimarySessionState } from "../../environments/primary";
4133import { useEnvironmentSettings, useUpdateEnvironmentSettings } from "../../hooks/useSettings";
34import { EnvironmentMachineIcon } from "../EnvironmentMachineIcon";
4235import { cn } from "../../lib/utils";
4336import { resolveAppModelSelectionState } from "../../modelSelection";
4437import {
......@@ -152,14 +145,6 @@ function ProviderLastChecked({ lastCheckedAt }: { lastCheckedAt: string | null }
152145 );
153146}
154147
155function providerEnvironmentIcon(environment: EnvironmentPresentation) {
156 if (environment.entry.target._tag === "PrimaryConnectionTarget") return MonitorIcon;
157 if (environment.entry.target._tag === "RelayConnectionTarget") return CloudIcon;
158 if (environment.entry.target._tag === "SshConnectionTarget") return TerminalIcon;
159 if (isDesktopLocalConnectionTarget(environment.entry.target)) return LaptopIcon;
160 return CloudIcon;
161}
162
163148function providerEnvironmentDetail(environment: EnvironmentPresentation): string {
164149 if (environment.entry.target._tag === "PrimaryConnectionTarget") return "Primary device";
165150 if (environment.relayManaged) return "T3 Connect";
......@@ -259,7 +244,7 @@ function ProviderSettingsPanelContent() {
259244 className="flex h-full w-max min-w-full border-b border-border/70 px-1"
260245 >
261246 {options.map((environment) => {
262 const Icon = providerEnvironmentIcon(environment);
247 const machine = resolveEnvironmentMachineKind(environment.serverConfig);
263248 const selected = environment.environmentId === effectiveEnvironmentId;
264249 const detail = providerEnvironmentDetail(environment);
265250 const statusText = connectionStatusText(environment.connection);
......@@ -273,7 +258,11 @@ function ProviderSettingsPanelContent() {
273258 className={cn(providerSettingsTabClassName(selected), "gap-2 text-left")}
274259 onClick={() => setSelectedEnvironmentId(environment.environmentId)}
275260 >
276 <Icon className="size-3.5 shrink-0" aria-hidden />
261 <EnvironmentMachineIcon
262 kind={machine}
263 className="size-3.5 shrink-0"
264 aria-hidden
265 />
277266 <span className="max-w-40 truncate">{environment.label}</span>
278267 {environment.connection.phase !== "connected" ? (
279268 <ConnectionStatusDot
docs/user/thread-sidebar.md+4-2
......@@ -59,8 +59,10 @@ When you are connected to more than one environment, every thread that lives som
5959the machine you are on wears a small icon for that machine at the end of its row: a server, a cloud
6060VM, a desktop, a laptop, a Mac mini, or a Mac Studio. In the hosted web app and the mobile app,
6161where every environment is remote, each row wears its machine so you can tell them apart at a
62glance. The same icon appears in the thread tooltip, the "Run on" picker, the pull request server
63filter, and the environment lists under **Settings → Connections**.
62glance. The same icon appears wherever an environment is named: the thread tooltip, the command
63palette, the "Run on" picker, the pull request server filter, the provider settings device tabs,
64and the environment lists under **Settings → Connections**. On mobile it appears in the thread
65lists, the archive, the new-task environment picker, and the Environments and storage settings.
6466
6567Servers pick the icon themselves from the hardware they run on. A Mac reports its model, a Linux
6668machine reports its chassis type and whether it is a virtual machine, and anything without a usable