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