| author | |
| committer | |
| log | 829c3db94830fc70b5754513b8a370f7301ca213 |
| tree | c06233d52836383e03d9ae02a55f6f65a8500212 |
| parent | 5eb4f452ee0fcde7b5bbc93d62de9118b70a33b2 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 { | ... | @@ -3,7 +3,11 @@ import type { |
| 3 | EnvironmentThreadShell, | 3 | EnvironmentThreadShell, |
| 4 | } from "@t3tools/client-runtime/state/shell"; | 4 | } from "@t3tools/client-runtime/state/shell"; |
| 5 | import { LegendList } from "@legendapp/list/react-native"; | 5 | import { LegendList } from "@legendapp/list/react-native"; |
| 6 | import type { EnvironmentId } from "@t3tools/contracts"; | 6 | import { |
| 7 | type EnvironmentId, | ||
| 8 | type EnvironmentMachineKind, | ||
| 9 | resolveEnvironmentMachineKind, | ||
| 10 | } from "@t3tools/contracts"; | ||
| 7 | import type { MenuAction } from "@react-native-menu/menu"; | 11 | import type { MenuAction } from "@react-native-menu/menu"; |
| 8 | import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; | 12 | import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; |
| 9 | import { SymbolView } from "../../components/AppSymbol"; | 13 | import { SymbolView } from "../../components/AppSymbol"; |
| ... | @@ -24,10 +28,12 @@ import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSw | ... | @@ -24,10 +28,12 @@ import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSw |
| 24 | import { AppText as Text } from "../../components/AppText"; | 28 | import { AppText as Text } from "../../components/AppText"; |
| 25 | import { ControlPillMenu } from "../../components/ControlPill"; | 29 | import { ControlPillMenu } from "../../components/ControlPill"; |
| 26 | import { EmptyState } from "../../components/EmptyState"; | 30 | import { EmptyState } from "../../components/EmptyState"; |
| 31 | import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol"; | ||
| 27 | import { ProjectFavicon } from "../../components/ProjectFavicon"; | 32 | import { ProjectFavicon } from "../../components/ProjectFavicon"; |
| 28 | import { useSafeAreaInsets } from "react-native-safe-area-context"; | 33 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 29 | import { relativeTime } from "../../lib/time"; | 34 | import { relativeTime } from "../../lib/time"; |
| 30 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | 35 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; |
| 36 | import { useServerConfigs } from "../../state/entities"; | ||
| 31 | import { ThreadSwipeable } from "../home/thread-swipe-actions"; | 37 | import { ThreadSwipeable } from "../home/thread-swipe-actions"; |
| 32 | import { | 38 | import { |
| 33 | createNativeMailSearchToolbarItem, | 39 | createNativeMailSearchToolbarItem, |
| ... | @@ -45,6 +51,7 @@ type ArchivedThreadListItem = | ... | @@ -45,6 +51,7 @@ type ArchivedThreadListItem = |
| 45 | readonly kind: "project"; | 51 | readonly kind: "project"; |
| 46 | readonly key: string; | 52 | readonly key: string; |
| 47 | readonly environmentLabel: string | null; | 53 | readonly environmentLabel: string | null; |
| 54 | readonly environmentMachine: EnvironmentMachineKind; | ||
| 48 | readonly project: EnvironmentProject; | 55 | readonly project: EnvironmentProject; |
| 49 | } | 56 | } |
| 50 | | { | 57 | | { |
| ... | @@ -360,6 +367,7 @@ function ArchivedThreadsHeader(props: { | ... | @@ -360,6 +367,7 @@ function ArchivedThreadsHeader(props: { |
| 360 | 367 | ||
| 361 | function ProjectGroupLabel(props: { | 368 | function ProjectGroupLabel(props: { |
| 362 | readonly environmentLabel: string | null; | 369 | readonly environmentLabel: string | null; |
| 370 | readonly environmentMachine: EnvironmentMachineKind; | ||
| 363 | readonly project: EnvironmentProject; | 371 | readonly project: EnvironmentProject; |
| 364 | }) { | 372 | }) { |
| 365 | return ( | 373 | return ( |
| ... | @@ -378,9 +386,16 @@ function ProjectGroupLabel(props: { | ... | @@ -378,9 +386,16 @@ function ProjectGroupLabel(props: { |
| 378 | {props.project.title} | 386 | {props.project.title} |
| 379 | </Text> | 387 | </Text> |
| 380 | {props.environmentLabel ? ( | 388 | {props.environmentLabel ? ( |
| 381 | <Text className="max-w-[42%] text-2xs text-foreground-tertiary" numberOfLines={1}> | 389 | <View className="max-w-[42%] flex-row items-center gap-1"> |
| 382 | {props.environmentLabel} | 390 | <EnvironmentMachineSymbol |
| 383 | </Text> | 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> | ||
| 384 | ) : null} | 399 | ) : null} |
| 385 | </View> | 400 | </View> |
| 386 | ); | 401 | ); |
| ... | @@ -517,6 +532,7 @@ export function ArchivedThreadsScreen(props: { | ... | @@ -517,6 +532,7 @@ export function ArchivedThreadsScreen(props: { |
| 517 | ), | 532 | ), |
| 518 | [props.environments], | 533 | [props.environments], |
| 519 | ); | 534 | ); |
| 535 | const serverConfigs = useServerConfigs(); | ||
| 520 | const listItems = useMemo<ReadonlyArray<ArchivedThreadListItem>>(() => { | 536 | const listItems = useMemo<ReadonlyArray<ArchivedThreadListItem>>(() => { |
| 521 | const items: ArchivedThreadListItem[] = []; | 537 | const items: ArchivedThreadListItem[] = []; |
| 522 | for (const group of props.groups) { | 538 | for (const group of props.groups) { |
| ... | @@ -525,6 +541,9 @@ export function ArchivedThreadsScreen(props: { | ... | @@ -525,6 +541,9 @@ export function ArchivedThreadsScreen(props: { |
| 525 | kind: "project", | 541 | kind: "project", |
| 526 | key: `${group.key}:project`, | 542 | key: `${group.key}:project`, |
| 527 | environmentLabel, | 543 | environmentLabel, |
| 544 | environmentMachine: resolveEnvironmentMachineKind( | ||
| 545 | serverConfigs.get(group.project.environmentId) ?? null, | ||
| 546 | ), | ||
| 528 | project: group.project, | 547 | project: group.project, |
| 529 | }); | 548 | }); |
| 530 | 549 | ||
| ... | @@ -540,7 +559,7 @@ export function ArchivedThreadsScreen(props: { | ... | @@ -540,7 +559,7 @@ export function ArchivedThreadsScreen(props: { |
| 540 | }); | 559 | }); |
| 541 | } | 560 | } |
| 542 | return items; | 561 | return items; |
| 543 | }, [environmentLabelsById, props.groups]); | 562 | }, [environmentLabelsById, props.groups, serverConfigs]); |
| 544 | const handleSwipeableWillOpen = useCallback((methods: SwipeableMethods) => { | 563 | const handleSwipeableWillOpen = useCallback((methods: SwipeableMethods) => { |
| 545 | if (openSwipeableRef.current && openSwipeableRef.current !== methods) { | 564 | if (openSwipeableRef.current && openSwipeableRef.current !== methods) { |
| 546 | openSwipeableRef.current.close(); | 565 | openSwipeableRef.current.close(); |
| ... | @@ -559,7 +578,11 @@ export function ArchivedThreadsScreen(props: { | ... | @@ -559,7 +578,11 @@ export function ArchivedThreadsScreen(props: { |
| 559 | if (item.kind === "project") { | 578 | if (item.kind === "project") { |
| 560 | return ( | 579 | return ( |
| 561 | <View className="pt-4"> | 580 | <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 | /> | ||
| 563 | </View> | 586 | </View> |
| 564 | ); | 587 | ); |
| 565 | } | 588 | } |
apps/mobile/src/features/projects/AddProjectScreen.tsx+13-5| ... | @@ -31,7 +31,13 @@ import { | ... | @@ -31,7 +31,13 @@ import { |
| 31 | inferProjectTitleFromPath, | 31 | inferProjectTitleFromPath, |
| 32 | isWindowsPlatform, | 32 | isWindowsPlatform, |
| 33 | } from "@t3tools/client-runtime/state/projects"; | 33 | } from "@t3tools/client-runtime/state/projects"; |
| 34 | import { CommandId, type EnvironmentId, ProjectId } from "@t3tools/contracts"; | 34 | import { |
| 35 | CommandId, | ||
| 36 | type EnvironmentId, | ||
| 37 | type EnvironmentMachineKind, | ||
| 38 | ProjectId, | ||
| 39 | resolveEnvironmentMachineKind, | ||
| 40 | } from "@t3tools/contracts"; | ||
| 35 | import { CommonActions, StackActions, useNavigation } from "@react-navigation/native"; | 41 | import { CommonActions, StackActions, useNavigation } from "@react-navigation/native"; |
| 36 | import { SymbolView } from "../../components/AppSymbol"; | 42 | import { SymbolView } from "../../components/AppSymbol"; |
| 37 | import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; | 43 | import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; |
| ... | @@ -49,6 +55,7 @@ import { projectEnvironment } from "../../state/projects"; | ... | @@ -49,6 +55,7 @@ import { projectEnvironment } from "../../state/projects"; |
| 49 | import { useEnvironmentQuery } from "../../state/query"; | 55 | import { useEnvironmentQuery } from "../../state/query"; |
| 50 | import { sourceControlEnvironment } from "../../state/sourceControl"; | 56 | import { sourceControlEnvironment } from "../../state/sourceControl"; |
| 51 | import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText"; | 57 | import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText"; |
| 58 | import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol"; | ||
| 52 | import { ErrorBanner } from "../../components/ErrorBanner"; | 59 | import { ErrorBanner } from "../../components/ErrorBanner"; |
| 53 | import { SourceControlIcon } from "../../components/SourceControlIcon"; | 60 | import { SourceControlIcon } from "../../components/SourceControlIcon"; |
| 54 | import { uuidv4 } from "../../lib/uuid"; | 61 | import { uuidv4 } from "../../lib/uuid"; |
| ... | @@ -65,6 +72,7 @@ interface EnvironmentOption { | ... | @@ -65,6 +72,7 @@ interface EnvironmentOption { |
| 65 | readonly environmentId: EnvironmentId; | 72 | readonly environmentId: EnvironmentId; |
| 66 | readonly label: string; | 73 | readonly label: string; |
| 67 | readonly platform: string; | 74 | readonly platform: string; |
| 75 | readonly machine: EnvironmentMachineKind; | ||
| 68 | readonly baseDirectory: string | null; | 76 | readonly baseDirectory: string | null; |
| 69 | readonly connectionState: EnvironmentConnectionPhase; | 77 | readonly connectionState: EnvironmentConnectionPhase; |
| 70 | readonly connectionError: string | null; | 78 | readonly connectionError: string | null; |
| ... | @@ -352,6 +360,7 @@ function useEnvironmentOptions(): ReadonlyArray<EnvironmentOption> { | ... | @@ -352,6 +360,7 @@ function useEnvironmentOptions(): ReadonlyArray<EnvironmentOption> { |
| 352 | environmentId: connection.environmentId, | 360 | environmentId: connection.environmentId, |
| 353 | label: connection.environmentLabel, | 361 | label: connection.environmentLabel, |
| 354 | platform: platformFromOs(config?.environment.platform.os ?? null), | 362 | platform: platformFromOs(config?.environment.platform.os ?? null), |
| 363 | machine: resolveEnvironmentMachineKind(config ?? null), | ||
| 355 | baseDirectory: config?.settings.addProjectBaseDirectory ?? null, | 364 | baseDirectory: config?.settings.addProjectBaseDirectory ?? null, |
| 356 | connectionState: runtime?.connectionState ?? "available", | 365 | connectionState: runtime?.connectionState ?? "available", |
| 357 | connectionError: runtime?.connectionError ?? null, | 366 | connectionError: runtime?.connectionError ?? null, |
| ... | @@ -490,11 +499,10 @@ export function AddProjectSourceScreen() { | ... | @@ -490,11 +499,10 @@ export function AddProjectSourceScreen() { |
| 490 | }) | 499 | }) |
| 491 | } | 500 | } |
| 492 | icon={ | 501 | icon={ |
| 493 | <SymbolView | 502 | <EnvironmentMachineSymbol |
| 494 | name="server.rack" | 503 | kind={environment.machine} |
| 495 | size={17} | 504 | size={17} |
| 496 | tintColorClassName={"accent-icon"} | 505 | tintColorClassName="accent-icon" |
| 497 | type="monochrome" | ||
| 498 | /> | 506 | /> |
| 499 | } | 507 | } |
| 500 | selected={environment.environmentId === selectedEnvironment?.environmentId} | 508 | selected={environment.environmentId === selectedEnvironment?.environmentId} |
apps/mobile/src/features/settings/SettingsClientStorageRouteScreen.tsx+9-7| ... | @@ -1,4 +1,5 @@ | ... | @@ -1,4 +1,5 @@ |
| 1 | import { useAtomSet, useAtomValue } from "@effect/atom-react"; | 1 | import { useAtomSet, useAtomValue } from "@effect/atom-react"; |
| 2 | import { type EnvironmentMachineKind, resolveEnvironmentMachineKind } from "@t3tools/contracts"; | ||
| 2 | import { AsyncResult } from "effect/unstable/reactivity"; | 3 | import { AsyncResult } from "effect/unstable/reactivity"; |
| 3 | import { useMemo } from "react"; | 4 | import { useMemo } from "react"; |
| 4 | import { ActivityIndicator, Alert, Pressable, ScrollView, View } from "react-native"; | 5 | import { ActivityIndicator, Alert, Pressable, ScrollView, View } from "react-native"; |
| ... | @@ -6,11 +7,13 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; | ... | @@ -6,11 +7,13 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 6 | 7 | ||
| 7 | import { AppText as Text } from "../../components/AppText"; | 8 | import { AppText as Text } from "../../components/AppText"; |
| 8 | import { SymbolView } from "../../components/AppSymbol"; | 9 | import { SymbolView } from "../../components/AppSymbol"; |
| 10 | import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol"; | ||
| 9 | import { | 11 | import { |
| 10 | clearClientCacheAtom, | 12 | clearClientCacheAtom, |
| 11 | clientCacheSummaryAtom, | 13 | clientCacheSummaryAtom, |
| 12 | type EnvironmentClientCacheSummary, | 14 | type EnvironmentClientCacheSummary, |
| 13 | } from "../../state/client-cache-state"; | 15 | } from "../../state/client-cache-state"; |
| 16 | import { useServerConfigs } from "../../state/entities"; | ||
| 14 | import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; | 17 | import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; |
| 15 | import { SettingsSection } from "./components/SettingsSection"; | 18 | import { SettingsSection } from "./components/SettingsSection"; |
| 16 | 19 | ||
| ... | @@ -20,6 +23,7 @@ export function SettingsClientStorageRouteScreen() { | ... | @@ -20,6 +23,7 @@ export function SettingsClientStorageRouteScreen() { |
| 20 | const clearResult = useAtomValue(clearClientCacheAtom); | 23 | const clearResult = useAtomValue(clearClientCacheAtom); |
| 21 | const clearCache = useAtomSet(clearClientCacheAtom); | 24 | const clearCache = useAtomSet(clearClientCacheAtom); |
| 22 | const { savedConnectionsById } = useSavedRemoteConnections(); | 25 | const { savedConnectionsById } = useSavedRemoteConnections(); |
| 26 | const serverConfigs = useServerConfigs(); | ||
| 23 | const isClearing = clearResult.waiting; | 27 | const isClearing = clearResult.waiting; |
| 24 | const summary = AsyncResult.isSuccess(summaryResult) ? summaryResult.value : null; | 28 | const summary = AsyncResult.isSuccess(summaryResult) ? summaryResult.value : null; |
| 25 | const environmentSummaries = useMemo( | 29 | const environmentSummaries = useMemo( |
| ... | @@ -106,6 +110,9 @@ export function SettingsClientStorageRouteScreen() { | ... | @@ -106,6 +110,9 @@ export function SettingsClientStorageRouteScreen() { |
| 106 | savedConnectionsById[environment.environmentId]?.environmentLabel ?? | 110 | savedConnectionsById[environment.environmentId]?.environmentLabel ?? |
| 107 | environment.environmentId | 111 | environment.environmentId |
| 108 | } | 112 | } |
| 113 | machine={resolveEnvironmentMachineKind( | ||
| 114 | serverConfigs.get(environment.environmentId) ?? null, | ||
| 115 | )} | ||
| 109 | disabled={isClearing} | 116 | disabled={isClearing} |
| 110 | first={index === 0} | 117 | first={index === 0} |
| 111 | onClear={() => confirmClearEnvironment(environment)} | 118 | onClear={() => confirmClearEnvironment(environment)} |
| ... | @@ -169,6 +176,7 @@ export function SettingsClientStorageRouteScreen() { | ... | @@ -169,6 +176,7 @@ export function SettingsClientStorageRouteScreen() { |
| 169 | function CacheEnvironmentRow(props: { | 176 | function CacheEnvironmentRow(props: { |
| 170 | readonly environment: EnvironmentClientCacheSummary; | 177 | readonly environment: EnvironmentClientCacheSummary; |
| 171 | readonly environmentLabel: string; | 178 | readonly environmentLabel: string; |
| 179 | readonly machine: EnvironmentMachineKind; | ||
| 172 | readonly disabled: boolean; | 180 | readonly disabled: boolean; |
| 173 | readonly first: boolean; | 181 | readonly first: boolean; |
| 174 | readonly onClear: () => void; | 182 | readonly onClear: () => void; |
| ... | @@ -181,13 +189,7 @@ function CacheEnvironmentRow(props: { | ... | @@ -181,13 +189,7 @@ function CacheEnvironmentRow(props: { |
| 181 | : "border-t border-border flex-row items-center gap-3 p-4" | 189 | : "border-t border-border flex-row items-center gap-3 p-4" |
| 182 | } | 190 | } |
| 183 | > | 191 | > |
| 184 | <SymbolView | 192 | <EnvironmentMachineSymbol kind={props.machine} size={22} tintColorClassName="accent-icon" /> |
| 185 | name="desktopcomputer" | ||
| 186 | size={22} | ||
| 187 | tintColorClassName={"accent-icon"} | ||
| 188 | type="monochrome" | ||
| 189 | weight="regular" | ||
| 190 | /> | ||
| 191 | <Text className="min-w-0 flex-1 text-base text-foreground" numberOfLines={1}> | 193 | <Text className="min-w-0 flex-1 text-base text-foreground" numberOfLines={1}> |
| 192 | {props.environmentLabel} | 194 | {props.environmentLabel} |
| 193 | </Text> | 195 | </Text> |
apps/mobile/src/features/threads/NewTaskContextPickerScreens.tsx+19-5| ... | @@ -1,4 +1,5 @@ | ... | @@ -1,4 +1,5 @@ |
| 1 | import type { VcsRef } from "@t3tools/client-runtime/state/vcs"; | 1 | import type { VcsRef } from "@t3tools/client-runtime/state/vcs"; |
| 2 | import { resolveEnvironmentMachineKind } from "@t3tools/contracts"; | ||
| 2 | import { LegendList } from "@legendapp/list/react-native"; | 3 | import { LegendList } from "@legendapp/list/react-native"; |
| 3 | import { | 4 | import { |
| 4 | isAtomCommandInterrupted, | 5 | isAtomCommandInterrupted, |
| ... | @@ -21,9 +22,11 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; | ... | @@ -21,9 +22,11 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 21 | import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; | 22 | import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; |
| 22 | import { SymbolView } from "../../components/AppSymbol"; | 23 | import { SymbolView } from "../../components/AppSymbol"; |
| 23 | import { AppText as Text } from "../../components/AppText"; | 24 | import { AppText as Text } from "../../components/AppText"; |
| 25 | import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol"; | ||
| 24 | import { ThemedSwitch } from "../../components/ThemedSwitch"; | 26 | import { ThemedSwitch } from "../../components/ThemedSwitch"; |
| 25 | import { cn } from "../../lib/cn"; | 27 | import { cn } from "../../lib/cn"; |
| 26 | import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; | 28 | import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; |
| 29 | import { useServerConfigs } from "../../state/entities"; | ||
| 27 | import { useAtomCommand } from "../../state/use-atom-command"; | 30 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 28 | import { vcsEnvironment } from "../../state/vcs"; | 31 | import { vcsEnvironment } from "../../state/vcs"; |
| 29 | import { | 32 | import { |
| ... | @@ -35,7 +38,7 @@ import { branchBadgeLabel, useNewTaskFlow } from "./new-task-flow-provider"; | ... | @@ -35,7 +38,7 @@ import { branchBadgeLabel, useNewTaskFlow } from "./new-task-flow-provider"; |
| 35 | import { shouldCheckoutNewTaskBranch } from "./new-task-context-presentation"; | 38 | import { shouldCheckoutNewTaskBranch } from "./new-task-context-presentation"; |
| 36 | 39 | ||
| 37 | function SelectionRow(props: { | 40 | function SelectionRow(props: { |
| 38 | readonly icon?: "arrow.triangle.branch" | "desktopcomputer"; | 41 | readonly icon?: "arrow.triangle.branch" | ReactNode; |
| 39 | readonly onPress: () => void; | 42 | readonly onPress: () => void; |
| 40 | readonly disabled?: boolean; | 43 | readonly disabled?: boolean; |
| 41 | readonly selected: boolean; | 44 | readonly selected: boolean; |
| ... | @@ -56,14 +59,16 @@ function SelectionRow(props: { | ... | @@ -56,14 +59,16 @@ function SelectionRow(props: { |
| 56 | onPress={props.onPress} | 59 | onPress={props.onPress} |
| 57 | style={{ opacity: props.disabled ? 0.45 : 1 }} | 60 | style={{ opacity: props.disabled ? 0.45 : 1 }} |
| 58 | > | 61 | > |
| 59 | {props.icon ? ( | 62 | {props.icon === "arrow.triangle.branch" ? ( |
| 60 | <SymbolView | 63 | <SymbolView |
| 61 | name={props.icon} | 64 | name="arrow.triangle.branch" |
| 62 | size={17} | 65 | size={17} |
| 63 | tintColorClassName={"accent-icon-muted"} | 66 | tintColorClassName={"accent-icon-muted"} |
| 64 | type="monochrome" | 67 | type="monochrome" |
| 65 | /> | 68 | /> |
| 66 | ) : null} | 69 | ) : ( |
| 70 | (props.icon ?? null) | ||
| 71 | )} | ||
| 67 | <View className="min-w-0 flex-1 gap-0.5"> | 72 | <View className="min-w-0 flex-1 gap-0.5"> |
| 68 | <Text className="text-base font-t3-medium text-foreground" numberOfLines={1}> | 73 | <Text className="text-base font-t3-medium text-foreground" numberOfLines={1}> |
| 69 | {props.title} | 74 | {props.title} |
| ... | @@ -145,6 +150,7 @@ export function NewTaskEnvironmentPickerRouteScreen() { | ... | @@ -145,6 +150,7 @@ export function NewTaskEnvironmentPickerRouteScreen() { |
| 145 | const flow = useNewTaskFlow(); | 150 | const flow = useNewTaskFlow(); |
| 146 | const navigation = useNavigation(); | 151 | const navigation = useNavigation(); |
| 147 | const insets = useSafeAreaInsets(); | 152 | const insets = useSafeAreaInsets(); |
| 153 | const serverConfigs = useServerConfigs(); | ||
| 148 | 154 | ||
| 149 | return ( | 155 | return ( |
| 150 | <View className="flex-1 bg-sheet" collapsable={false}> | 156 | <View className="flex-1 bg-sheet" collapsable={false}> |
| ... | @@ -170,7 +176,15 @@ export function NewTaskEnvironmentPickerRouteScreen() { | ... | @@ -170,7 +176,15 @@ export function NewTaskEnvironmentPickerRouteScreen() { |
| 170 | {flow.environments.map((environment, index) => ( | 176 | {flow.environments.map((environment, index) => ( |
| 171 | <SelectionRow | 177 | <SelectionRow |
| 172 | key={String(environment.environmentId)} | 178 | 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 | } | ||
| 174 | isLast={index === flow.environments.length - 1} | 188 | isLast={index === flow.environments.length - 1} |
| 175 | onPress={() => { | 189 | onPress={() => { |
| 176 | void Haptics.selectionAsync(); | 190 | void Haptics.selectionAsync(); |
apps/mobile/src/features/threads/NewTaskDraftScreen.tsx+12-2| ... | @@ -24,7 +24,10 @@ import { | ... | @@ -24,7 +24,10 @@ import { |
| 24 | isAtomCommandInterrupted, | 24 | isAtomCommandInterrupted, |
| 25 | squashAtomCommandFailure, | 25 | squashAtomCommandFailure, |
| 26 | } from "@t3tools/client-runtime/state/runtime"; | 26 | } from "@t3tools/client-runtime/state/runtime"; |
| 27 | import { PROVIDER_SEND_TURN_MAX_ATTACHMENTS } from "@t3tools/contracts"; | 27 | import { |
| 28 | PROVIDER_SEND_TURN_MAX_ATTACHMENTS, | ||
| 29 | resolveEnvironmentMachineKind, | ||
| 30 | } from "@t3tools/contracts"; | ||
| 28 | 31 | ||
| 29 | import { ComposerEditor, type ComposerEditorHandle } from "../../components/ComposerEditor"; | 32 | import { ComposerEditor, type ComposerEditorHandle } from "../../components/ComposerEditor"; |
| 30 | import { | 33 | import { |
| ... | @@ -36,6 +39,7 @@ import { | ... | @@ -36,6 +39,7 @@ import { |
| 36 | import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; | 39 | import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; |
| 37 | import { ComposerAttachmentButton } from "../../components/ComposerAttachmentButton"; | 40 | import { ComposerAttachmentButton } from "../../components/ComposerAttachmentButton"; |
| 38 | import { ComposerAttachmentStrip } from "../../components/ComposerAttachmentStrip"; | 41 | import { ComposerAttachmentStrip } from "../../components/ComposerAttachmentStrip"; |
| 42 | import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol"; | ||
| 39 | import { | 43 | import { |
| 40 | composerAttachmentUploadBlockReason, | 44 | composerAttachmentUploadBlockReason, |
| 41 | composerAttachmentUploadsAtom, | 45 | composerAttachmentUploadsAtom, |
| ... | @@ -1129,7 +1133,13 @@ export function NewTaskDraftScreen(props: { | ... | @@ -1129,7 +1133,13 @@ export function NewTaskDraftScreen(props: { |
| 1129 | accessibilityLabel={`Environment: ${selectedEnvironmentLabel}`} | 1133 | accessibilityLabel={`Environment: ${selectedEnvironmentLabel}`} |
| 1130 | chevronDirection="right" | 1134 | chevronDirection="right" |
| 1131 | disabled={isComposerInteractionLocked || voiceInput.isBusy} | 1135 | 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 | } | ||
| 1133 | label={`on ${selectedEnvironmentLabel}`} | 1143 | label={`on ${selectedEnvironmentLabel}`} |
| 1134 | maxWidth={260} | 1144 | maxWidth={260} |
| 1135 | onPress={ | 1145 | onPress={ |
apps/web/src/components/BranchToolbarEnvironmentSelector.tsx+10-16| ... | @@ -1,8 +1,8 @@ | ... | @@ -1,8 +1,8 @@ |
| 1 | import type { EnvironmentId } from "@t3tools/contracts"; | 1 | import type { EnvironmentId } from "@t3tools/contracts"; |
| 2 | import { CloudIcon, MonitorIcon } from "lucide-react"; | ||
| 3 | import { memo, useMemo } from "react"; | 2 | import { memo, useMemo } from "react"; |
| 4 | 3 | ||
| 5 | import type { EnvironmentOption } from "./BranchToolbar.logic"; | 4 | import type { EnvironmentOption } from "./BranchToolbar.logic"; |
| 5 | import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon"; | ||
| 6 | import { | 6 | import { |
| 7 | Select, | 7 | Select, |
| 8 | SelectGroup, | 8 | SelectGroup, |
| ... | @@ -52,11 +52,10 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir | ... | @@ -52,11 +52,10 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir |
| 52 | 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" | 52 | 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" |
| 53 | data-composer-context-control | 53 | data-composer-context-control |
| 54 | > | 54 | > |
| 55 | {activeEnvironment?.isPrimary ? ( | 55 | <EnvironmentMachineIcon |
| 56 | <MonitorIcon className="size-3 shrink-0" /> | 56 | kind={activeEnvironment?.machine ?? "server"} |
| 57 | ) : ( | 57 | className="size-3 shrink-0" |
| 58 | <CloudIcon className="size-3 shrink-0" /> | 58 | /> |
| 59 | )} | ||
| 60 | <span | 59 | <span |
| 61 | data-composer-label | 60 | data-composer-label |
| 62 | className="min-w-0 max-w-[240px] group-data-[compact]/composer-context:max-w-0" | 61 | 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 | ... | @@ -86,11 +85,10 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir |
| 86 | aria-label="Run on" | 85 | aria-label="Run on" |
| 87 | data-composer-context-control | 86 | data-composer-context-control |
| 88 | > | 87 | > |
| 89 | {activeEnvironment?.isPrimary ? ( | 88 | <EnvironmentMachineIcon |
| 90 | <MonitorIcon className="size-3 shrink-0" /> | 89 | kind={activeEnvironment?.machine ?? "server"} |
| 91 | ) : ( | 90 | className="size-3 shrink-0" |
| 92 | <CloudIcon className="size-3 shrink-0" /> | 91 | /> |
| 93 | )} | ||
| 94 | <span | 92 | <span |
| 95 | data-composer-label | 93 | data-composer-label |
| 96 | className="min-w-0 max-w-[240px] group-data-[compact]/composer-context:max-w-0" | 94 | 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 | ... | @@ -109,11 +107,7 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir |
| 109 | {availableEnvironments.map((env) => ( | 107 | {availableEnvironments.map((env) => ( |
| 110 | <SelectItem key={env.environmentId} value={env.environmentId}> | 108 | <SelectItem key={env.environmentId} value={env.environmentId}> |
| 111 | <span className="inline-flex items-center gap-1.5"> | 109 | <span className="inline-flex items-center gap-1.5"> |
| 112 | {env.isPrimary ? ( | 110 | <EnvironmentMachineIcon kind={env.machine} className="size-3" /> |
| 113 | <MonitorIcon className="size-3" /> | ||
| 114 | ) : ( | ||
| 115 | <CloudIcon className="size-3" /> | ||
| 116 | )} | ||
| 117 | {env.label} | 111 | {env.label} |
| 118 | </span> | 112 | </span> |
| 119 | </SelectItem> | 113 | </SelectItem> |
apps/web/src/components/CommandPalette.tsx+9-2| ... | @@ -36,6 +36,7 @@ import { | ... | @@ -36,6 +36,7 @@ import { |
| 36 | type SourceControlProviderKind, | 36 | type SourceControlProviderKind, |
| 37 | type SourceControlRepositoryInfo, | 37 | type SourceControlRepositoryInfo, |
| 38 | PRIMARY_LOCAL_ENVIRONMENT_ID, | 38 | PRIMARY_LOCAL_ENVIRONMENT_ID, |
| 39 | resolveEnvironmentMachineKind, | ||
| 39 | } from "@t3tools/contracts"; | 40 | } from "@t3tools/contracts"; |
| 40 | import { useLocation, useNavigate, useParams } from "@tanstack/react-router"; | 41 | import { useLocation, useNavigate, useParams } from "@tanstack/react-router"; |
| 41 | import * as Option from "effect/Option"; | 42 | import * as Option from "effect/Option"; |
| ... | @@ -48,7 +49,6 @@ import { | ... | @@ -48,7 +49,6 @@ import { |
| 48 | LinkIcon, | 49 | LinkIcon, |
| 49 | MessageSquareIcon, | 50 | MessageSquareIcon, |
| 50 | PaletteIcon, | 51 | PaletteIcon, |
| 51 | ServerIcon, | ||
| 52 | SettingsIcon, | 52 | SettingsIcon, |
| 53 | SquarePenIcon, | 53 | SquarePenIcon, |
| 54 | TextSearchIcon, | 54 | TextSearchIcon, |
| ... | @@ -145,6 +145,7 @@ import { resolveEnvironmentOptionLabel } from "./BranchToolbar.logic"; | ... | @@ -145,6 +145,7 @@ import { resolveEnvironmentOptionLabel } from "./BranchToolbar.logic"; |
| 145 | import { CommandPaletteContent } from "./CommandPaletteContent"; | 145 | import { CommandPaletteContent } from "./CommandPaletteContent"; |
| 146 | import { CommandPaletteResults } from "./CommandPaletteResults"; | 146 | import { CommandPaletteResults } from "./CommandPaletteResults"; |
| 147 | import { AzureDevOpsIcon, BitbucketIcon, GitHubIcon, GitLabIcon } from "./Icons"; | 147 | import { AzureDevOpsIcon, BitbucketIcon, GitHubIcon, GitLabIcon } from "./Icons"; |
| 148 | import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon"; | ||
| 148 | import { ProjectFavicon } from "./ProjectFavicon"; | 149 | import { ProjectFavicon } from "./ProjectFavicon"; |
| 149 | import { ProjectFilePicker } from "./files/ProjectFilePicker"; | 150 | import { ProjectFilePicker } from "./files/ProjectFilePicker"; |
| 150 | import { ProjectContentSearchDialog } from "./search/ProjectContentSearchDialog"; | 151 | import { ProjectContentSearchDialog } from "./search/ProjectContentSearchDialog"; |
| ... | @@ -738,6 +739,7 @@ function OpenCommandPaletteDialog(props: { | ... | @@ -738,6 +739,7 @@ function OpenCommandPaletteDialog(props: { |
| 738 | : isLocal | 739 | : isLocal |
| 739 | ? `${environment.label} (Local)` | 740 | ? `${environment.label} (Local)` |
| 740 | : environment.label, | 741 | : environment.label, |
| 742 | machine: resolveEnvironmentMachineKind(environment.serverConfig), | ||
| 741 | }, | 743 | }, |
| 742 | ] as const; | 744 | ] as const; |
| 743 | }), | 745 | }), |
| ... | @@ -1119,12 +1121,17 @@ function OpenCommandPaletteDialog(props: { | ... | @@ -1119,12 +1121,17 @@ function OpenCommandPaletteDialog(props: { |
| 1119 | const location = projectEnvironmentLocationById.get(project.environmentId) ?? { | 1121 | const location = projectEnvironmentLocationById.get(project.environmentId) ?? { |
| 1120 | kind: "remote", | 1122 | kind: "remote", |
| 1121 | label: "Remote", | 1123 | label: "Remote", |
| 1124 | machine: "server" as const, | ||
| 1122 | }; | 1125 | }; |
| 1123 | return ( | 1126 | return ( |
| 1124 | <span className="flex min-w-0 items-center gap-1"> | 1127 | <span className="flex min-w-0 items-center gap-1"> |
| 1125 | <span className="inline-flex min-w-0 items-center gap-1"> | 1128 | <span className="inline-flex min-w-0 items-center gap-1"> |
| 1126 | {location.kind === "remote" ? ( | 1129 | {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 | /> | ||
| 1128 | ) : null} | 1135 | ) : null} |
| 1129 | <span className="truncate">{location.label}</span> | 1136 | <span className="truncate">{location.label}</span> |
| 1130 | </span> | 1137 | </span> |
apps/web/src/components/ThreadStatusIndicators.tsx+16-5| ... | @@ -4,12 +4,18 @@ import { | ... | @@ -4,12 +4,18 @@ import { |
| 4 | scopeThreadRef, | 4 | scopeThreadRef, |
| 5 | } from "@t3tools/client-runtime/environment"; | 5 | } from "@t3tools/client-runtime/environment"; |
| 6 | import { pullRequestDetailToVcsStatus } from "@t3tools/client-runtime/state/pull-requests"; | 6 | import { pullRequestDetailToVcsStatus } from "@t3tools/client-runtime/state/pull-requests"; |
| 7 | import type { EnvironmentId, ThreadLinkedPullRequest, VcsStatusResult } from "@t3tools/contracts"; | 7 | import { |
| 8 | type EnvironmentId, | ||
| 9 | resolveEnvironmentMachineKind, | ||
| 10 | type ThreadLinkedPullRequest, | ||
| 11 | type VcsStatusResult, | ||
| 12 | } from "@t3tools/contracts"; | ||
| 8 | import { Atom } from "effect/unstable/reactivity"; | 13 | import { Atom } from "effect/unstable/reactivity"; |
| 9 | import { CloudIcon, FolderGit2Icon, GitPullRequestIcon, TerminalIcon } from "lucide-react"; | 14 | import { FolderGit2Icon, GitPullRequestIcon, TerminalIcon } from "lucide-react"; |
| 10 | import { useMemo } from "react"; | 15 | import { useMemo } from "react"; |
| 11 | import { appAtomRegistry } from "../rpc/atomRegistry"; | 16 | import { appAtomRegistry } from "../rpc/atomRegistry"; |
| 12 | import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments"; | 17 | import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments"; |
| 18 | import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon"; | ||
| 13 | import { useProject } from "../state/entities"; | 19 | import { useProject } from "../state/entities"; |
| 14 | import { useEnvironmentQuery } from "../state/query"; | 20 | import { useEnvironmentQuery } from "../state/query"; |
| 15 | import { linkedPullRequestDetailAtom, useSharedPullRequestSummary } from "../state/pullRequests"; | 21 | import { linkedPullRequestDetailAtom, useSharedPullRequestSummary } from "../state/pullRequests"; |
| ... | @@ -602,10 +608,12 @@ export function ThreadRowTrailingStatus({ thread }: { thread: SidebarThreadSumma | ... | @@ -602,10 +608,12 @@ export function ThreadRowTrailingStatus({ thread }: { thread: SidebarThreadSumma |
| 602 | }); | 608 | }); |
| 603 | const environment = useEnvironment(thread.environmentId); | 609 | const environment = useEnvironment(thread.environmentId); |
| 604 | const primaryEnvironmentId = usePrimaryEnvironmentId(); | 610 | const primaryEnvironmentId = usePrimaryEnvironmentId(); |
| 605 | const isRemoteThread = | 611 | // No primary (the hosted app) means every thread is remote, and the machine |
| 606 | primaryEnvironmentId !== null && thread.environmentId !== primaryEnvironmentId; | 612 | // glyph is what tells the environments apart. |
| 613 | const isRemoteThread = thread.environmentId !== primaryEnvironmentId; | ||
| 607 | const remoteEnvLabel = environment?.label ?? null; | 614 | const remoteEnvLabel = environment?.label ?? null; |
| 608 | const threadEnvironmentLabel = isRemoteThread ? (remoteEnvLabel ?? "Remote") : null; | 615 | const threadEnvironmentLabel = isRemoteThread ? (remoteEnvLabel ?? "Remote") : null; |
| 616 | const remoteMachine = resolveEnvironmentMachineKind(environment?.serverConfig ?? null); | ||
| 609 | const terminalStatus = terminalStatusFromRunningIds(runningTerminalIds); | 617 | const terminalStatus = terminalStatusFromRunningIds(runningTerminalIds); |
| 610 | 618 | ||
| 611 | if (!terminalStatus && !isRemoteThread) { | 619 | if (!terminalStatus && !isRemoteThread) { |
| ... | @@ -642,7 +650,10 @@ export function ThreadRowTrailingStatus({ thread }: { thread: SidebarThreadSumma | ... | @@ -642,7 +650,10 @@ export function ThreadRowTrailingStatus({ thread }: { thread: SidebarThreadSumma |
| 642 | /> | 650 | /> |
| 643 | } | 651 | } |
| 644 | > | 652 | > |
| 645 | <CloudIcon className="size-3 text-muted-foreground/60" /> | 653 | <EnvironmentMachineIcon |
| 654 | kind={remoteMachine} | ||
| 655 | className="size-3 text-muted-foreground/60" | ||
| 656 | /> | ||
| 646 | </TooltipTrigger> | 657 | </TooltipTrigger> |
| 647 | <TooltipPopup side="top">{threadEnvironmentLabel}</TooltipPopup> | 658 | <TooltipPopup side="top">{threadEnvironmentLabel}</TooltipPopup> |
| 648 | </Tooltip> | 659 | </Tooltip> |
apps/web/src/components/settings/ProviderSettingsPanel.tsx+9-20| ... | @@ -12,6 +12,7 @@ import { | ... | @@ -12,6 +12,7 @@ import { |
| 12 | ProviderDriverKind, | 12 | ProviderDriverKind, |
| 13 | type ProviderInstanceConfig, | 13 | type ProviderInstanceConfig, |
| 14 | type ProviderInstanceId, | 14 | type ProviderInstanceId, |
| 15 | resolveEnvironmentMachineKind, | ||
| 15 | resolveProviderInstanceEnabled, | 16 | resolveProviderInstanceEnabled, |
| 16 | } from "@t3tools/contracts"; | 17 | } from "@t3tools/contracts"; |
| 17 | import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings"; | 18 | import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings"; |
| ... | @@ -23,22 +24,14 @@ import * as Arr from "effect/Array"; | ... | @@ -23,22 +24,14 @@ import * as Arr from "effect/Array"; |
| 23 | import * as Duration from "effect/Duration"; | 24 | import * as Duration from "effect/Duration"; |
| 24 | import * as Equal from "effect/Equal"; | 25 | import * as Equal from "effect/Equal"; |
| 25 | import * as Result from "effect/Result"; | 26 | import * as Result from "effect/Result"; |
| 26 | import { | 27 | import { ChevronDownIcon, LoaderIcon, PlusIcon, RefreshCwIcon } from "lucide-react"; |
| 27 | ChevronDownIcon, | ||
| 28 | CloudIcon, | ||
| 29 | LaptopIcon, | ||
| 30 | LoaderIcon, | ||
| 31 | MonitorIcon, | ||
| 32 | PlusIcon, | ||
| 33 | RefreshCwIcon, | ||
| 34 | TerminalIcon, | ||
| 35 | } from "lucide-react"; | ||
| 36 | import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; | 28 | import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; |
| 37 | 29 | ||
| 38 | import { isDesktopLocalConnectionTarget } from "../../connection/desktopLocal"; | 30 | import { isDesktopLocalConnectionTarget } from "../../connection/desktopLocal"; |
| 39 | import { isElectron } from "../../env"; | 31 | import { isElectron } from "../../env"; |
| 40 | import { usePrimarySessionState } from "../../environments/primary"; | 32 | import { usePrimarySessionState } from "../../environments/primary"; |
| 41 | import { useEnvironmentSettings, useUpdateEnvironmentSettings } from "../../hooks/useSettings"; | 33 | import { useEnvironmentSettings, useUpdateEnvironmentSettings } from "../../hooks/useSettings"; |
| 34 | import { EnvironmentMachineIcon } from "../EnvironmentMachineIcon"; | ||
| 42 | import { cn } from "../../lib/utils"; | 35 | import { cn } from "../../lib/utils"; |
| 43 | import { resolveAppModelSelectionState } from "../../modelSelection"; | 36 | import { resolveAppModelSelectionState } from "../../modelSelection"; |
| 44 | import { | 37 | import { |
| ... | @@ -152,14 +145,6 @@ function ProviderLastChecked({ lastCheckedAt }: { lastCheckedAt: string | null } | ... | @@ -152,14 +145,6 @@ function ProviderLastChecked({ lastCheckedAt }: { lastCheckedAt: string | null } |
| 152 | ); | 145 | ); |
| 153 | } | 146 | } |
| 154 | 147 | ||
| 155 | function 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 | |||
| 163 | function providerEnvironmentDetail(environment: EnvironmentPresentation): string { | 148 | function providerEnvironmentDetail(environment: EnvironmentPresentation): string { |
| 164 | if (environment.entry.target._tag === "PrimaryConnectionTarget") return "Primary device"; | 149 | if (environment.entry.target._tag === "PrimaryConnectionTarget") return "Primary device"; |
| 165 | if (environment.relayManaged) return "T3 Connect"; | 150 | if (environment.relayManaged) return "T3 Connect"; |
| ... | @@ -259,7 +244,7 @@ function ProviderSettingsPanelContent() { | ... | @@ -259,7 +244,7 @@ function ProviderSettingsPanelContent() { |
| 259 | className="flex h-full w-max min-w-full border-b border-border/70 px-1" | 244 | className="flex h-full w-max min-w-full border-b border-border/70 px-1" |
| 260 | > | 245 | > |
| 261 | {options.map((environment) => { | 246 | {options.map((environment) => { |
| 262 | const Icon = providerEnvironmentIcon(environment); | 247 | const machine = resolveEnvironmentMachineKind(environment.serverConfig); |
| 263 | const selected = environment.environmentId === effectiveEnvironmentId; | 248 | const selected = environment.environmentId === effectiveEnvironmentId; |
| 264 | const detail = providerEnvironmentDetail(environment); | 249 | const detail = providerEnvironmentDetail(environment); |
| 265 | const statusText = connectionStatusText(environment.connection); | 250 | const statusText = connectionStatusText(environment.connection); |
| ... | @@ -273,7 +258,11 @@ function ProviderSettingsPanelContent() { | ... | @@ -273,7 +258,11 @@ function ProviderSettingsPanelContent() { |
| 273 | className={cn(providerSettingsTabClassName(selected), "gap-2 text-left")} | 258 | className={cn(providerSettingsTabClassName(selected), "gap-2 text-left")} |
| 274 | onClick={() => setSelectedEnvironmentId(environment.environmentId)} | 259 | onClick={() => setSelectedEnvironmentId(environment.environmentId)} |
| 275 | > | 260 | > |
| 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 | /> | ||
| 277 | <span className="max-w-40 truncate">{environment.label}</span> | 266 | <span className="max-w-40 truncate">{environment.label}</span> |
| 278 | {environment.connection.phase !== "connected" ? ( | 267 | {environment.connection.phase !== "connected" ? ( |
| 279 | <ConnectionStatusDot | 268 | <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 | ... | @@ -59,8 +59,10 @@ When you are connected to more than one environment, every thread that lives som |
| 59 | the machine you are on wears a small icon for that machine at the end of its row: a server, a cloud | 59 | the machine you are on wears a small icon for that machine at the end of its row: a server, a cloud |
| 60 | VM, a desktop, a laptop, a Mac mini, or a Mac Studio. In the hosted web app and the mobile app, | 60 | VM, a desktop, a laptop, a Mac mini, or a Mac Studio. In the hosted web app and the mobile app, |
| 61 | where every environment is remote, each row wears its machine so you can tell them apart at a | 61 | where every environment is remote, each row wears its machine so you can tell them apart at a |
| 62 | glance. The same icon appears in the thread tooltip, the "Run on" picker, the pull request server | 62 | glance. The same icon appears wherever an environment is named: the thread tooltip, the command |
| 63 | filter, and the environment lists under **Settings → Connections**. | 63 | palette, the "Run on" picker, the pull request server filter, the provider settings device tabs, |
| 64 | and the environment lists under **Settings → Connections**. On mobile it appears in the thread | ||
| 65 | lists, the archive, the new-task environment picker, and the Environments and storage settings. | ||
| 64 | 66 | ||
| 65 | Servers pick the icon themselves from the hardware they run on. A Mac reports its model, a Linux | 67 | Servers pick the icon themselves from the hardware they run on. A Mac reports its model, a Linux |
| 66 | machine reports its chassis type and whether it is a virtual machine, and anything without a usable | 68 | machine reports its chassis type and whether it is a virtual machine, and anything without a usable |