| author | |
| committer | |
| log | c1b2ed6504bbd57440d03c6a5af2736f33be7c65 |
| tree | 02842faf01a5feb74e76aa6d21da159633b7ee03 |
| parent | 0f5a1513e09fb41e0d0f86157aff64cc3657975d |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>11 files changed, 153 insertions(+), 52 deletions(-)
apps/mobile/src/features/connection/CloudEnvironmentRows.tsx+34-13| ... | ... | @@ -7,6 +7,7 @@ import { |
| 7 | 7 | import { |
| 8 | 8 | type EnvironmentId, |
| 9 | 9 | type EnvironmentMachineKind, |
| 10 | type ExecutionEnvironmentDescriptor, | |
| 10 | 11 | resolveEnvironmentMachineKind, |
| 11 | 12 | } from "@t3tools/contracts"; |
| 12 | 13 | import { useAtomValue } from "@effect/atom-react"; |
| ... | ... | @@ -99,6 +100,16 @@ function CloudEnvironmentRowsContent( |
| 99 | 100 | [controller], |
| 100 | 101 | ); |
| 101 | 102 | |
| 103 | // The relay's health probe carries each server's descriptor, so a machine | |
| 104 | // can wear its detected glyph before this device ever connects to it. | |
| 105 | const discoveredDescriptors = new Map( | |
| 106 | controller.relayEnvironments.flatMap((entry) => | |
| 107 | entry.status?.descriptor === undefined | |
| 108 | ? [] | |
| 109 | : [[entry.environment.environmentId, entry.status.descriptor] as const], | |
| 110 | ), | |
| 111 | ); | |
| 112 | ||
| 102 | 113 | const handleToggleCloudError = useCallback((environmentId: string) => { |
| 103 | 114 | setExpandedErrorId((current) => (current === environmentId ? null : environmentId)); |
| 104 | 115 | }, []); |
| ... | ... | @@ -140,6 +151,7 @@ function CloudEnvironmentRowsContent( |
| 140 | 151 | <ConnectedCloudEnvironmentRow |
| 141 | 152 | key={environment.environmentId} |
| 142 | 153 | environment={environment} |
| 154 | descriptor={discoveredDescriptors.get(environment.environmentId)} | |
| 143 | 155 | borderTop={index !== 0} |
| 144 | 156 | onSetEnabled={(enabled) => |
| 145 | 157 | props.onSetEnvironmentEnabled(environment.environmentId, enabled) |
| ... | ... | @@ -210,6 +222,8 @@ function CloudEnvironmentRowsContent( |
| 210 | 222 | */ |
| 211 | 223 | function ConnectedCloudEnvironmentRow(props: { |
| 212 | 224 | readonly environment: ConnectedEnvironmentSummary; |
| 225 | /** Discovery's view of the server, for the glyph before the first connection. */ | |
| 226 | readonly descriptor: ExecutionEnvironmentDescriptor | undefined; | |
| 213 | 227 | readonly borderTop: boolean; |
| 214 | 228 | readonly errorExpanded: boolean; |
| 215 | 229 | readonly onSetEnabled: (enabled: boolean) => void; |
| ... | ... | @@ -233,7 +247,10 @@ function ConnectedCloudEnvironmentRow(props: { |
| 233 | 247 | connectionState={enabled || unsupported ? props.environment.connectionState : "available"} |
| 234 | 248 | errorExpanded={props.errorExpanded} |
| 235 | 249 | label={props.environment.environmentLabel} |
| 236 | machine={resolveEnvironmentMachineKind(serverConfig)} | |
| 250 | machine={resolveEnvironmentMachineKind( | |
| 251 | serverConfig ?? | |
| 252 | (props.descriptor === undefined ? null : { environment: props.descriptor }), | |
| 253 | )} | |
| 237 | 254 | onValueChange={props.onSetEnabled} |
| 238 | 255 | onToggleError={props.onToggleError} |
| 239 | 256 | disabled={unsupported} |
| ... | ... | @@ -266,6 +283,11 @@ function CloudEnvironmentRow(props: { |
| 266 | 283 | connectionState={presentation.connectionState} |
| 267 | 284 | errorExpanded={props.errorExpanded} |
| 268 | 285 | label={props.environment.environment.label} |
| 286 | machine={resolveEnvironmentMachineKind( | |
| 287 | props.environment.status?.descriptor === undefined | |
| 288 | ? null | |
| 289 | : { environment: props.environment.status.descriptor }, | |
| 290 | )} | |
| 269 | 291 | onValueChange={(enabled) => { |
| 270 | 292 | if (enabled) { |
| 271 | 293 | props.onConnect(); |
| ... | ... | @@ -287,8 +309,7 @@ function CloudEnvironmentRowShell(props: { |
| 287 | 309 | readonly disabled?: boolean; |
| 288 | 310 | readonly errorExpanded: boolean; |
| 289 | 311 | readonly label: string; |
| 290 | /** Absent for environments the relay lists but this device has not connected to. */ | |
| 291 | readonly machine?: EnvironmentMachineKind; | |
| 312 | readonly machine: EnvironmentMachineKind; | |
| 292 | 313 | readonly onToggleError: () => void; |
| 293 | 314 | readonly onValueChange: (enabled: boolean) => void; |
| 294 | 315 | readonly statusText?: string; |
| ... | ... | @@ -304,9 +325,11 @@ function CloudEnvironmentRowShell(props: { |
| 304 | 325 | error: props.connectionError, |
| 305 | 326 | traceId: props.connectionErrorTraceId, |
| 306 | 327 | }); |
| 307 | const statusClassName = props.connectionError | |
| 308 | ? "text-danger-foreground" | |
| 309 | : "text-foreground-muted"; | |
| 328 | // Unsupported is a compatibility note, not a failure, so it stays muted. | |
| 329 | const statusClassName = | |
| 330 | props.connectionError && props.connectionState !== "unsupported" | |
| 331 | ? "text-danger-foreground" | |
| 332 | : "text-foreground-muted"; | |
| 310 | 333 | const [errorMeasurement, setErrorMeasurement] = useState<{ |
| 311 | 334 | readonly text: string; |
| 312 | 335 | readonly lineCount: number; |
| ... | ... | @@ -344,13 +367,11 @@ function CloudEnvironmentRowShell(props: { |
| 344 | 367 | <View className="min-w-0 flex-1 gap-0.5"> |
| 345 | 368 | <View className="min-w-0 flex-row items-center gap-2"> |
| 346 | 369 | <ConnectionStatusDot state={props.connectionState} pulse={shouldPulse} size={7} /> |
| 347 | {props.machine ? ( | |
| 348 | <EnvironmentMachineSymbol | |
| 349 | kind={props.machine} | |
| 350 | size={14} | |
| 351 | tintColorClassName="accent-foreground-muted" | |
| 352 | /> | |
| 353 | ) : null} | |
| 370 | <EnvironmentMachineSymbol | |
| 371 | kind={props.machine} | |
| 372 | size={14} | |
| 373 | tintColorClassName="accent-foreground-muted" | |
| 374 | /> | |
| 354 | 375 | <Text |
| 355 | 376 | className="min-w-0 flex-shrink text-base font-t3-bold leading-snug text-foreground" |
| 356 | 377 | numberOfLines={1} |
apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx+2-2| ... | ... | @@ -50,8 +50,8 @@ export function ConnectionEnvironmentRow(props: { |
| 50 | 50 | const enabled = props.environment.isEnabled && !unsupported; |
| 51 | 51 | const statusLabel = connectionStatusLabel(props.environment); |
| 52 | 52 | const statusTraceId = enabled ? props.environment.connectionErrorTraceId : null; |
| 53 | const hasConnectionFailure = | |
| 54 | (enabled || unsupported) && props.environment.connectionError !== null; | |
| 53 | // Unsupported is a compatibility note, not a failure, so it stays muted. | |
| 54 | const hasConnectionFailure = enabled && props.environment.connectionError !== null; | |
| 55 | 55 | const isRetrying = |
| 56 | 56 | enabled && |
| 57 | 57 | (props.environment.connectionState === "connecting" || |
apps/mobile/src/features/connection/ConnectionStatusDot.tsx+3-1| ... | ... | @@ -18,7 +18,10 @@ function statusDotTone(state: ConnectionStatusDotState): { |
| 18 | 18 | readonly haloColor: string; |
| 19 | 19 | } { |
| 20 | 20 | switch (state) { |
| 21 | // Unsupported is not a failure: the machine is fine, this build just | |
| 22 | // cannot talk to it, so it wears the same neutral dot as "available". | |
| 21 | 23 | case "available": |
| 24 | case "unsupported": | |
| 22 | 25 | return { |
| 23 | 26 | dotColor: "#9ca3af", |
| 24 | 27 | haloColor: "rgba(156,163,175,0.42)", |
| ... | ... | @@ -35,7 +38,6 @@ function statusDotTone(state: ConnectionStatusDotState): { |
| 35 | 38 | haloColor: "rgba(245,158,11,0.5)", |
| 36 | 39 | }; |
| 37 | 40 | case "offline": |
| 38 | case "unsupported": | |
| 39 | 41 | case "error": |
| 40 | 42 | return { |
| 41 | 43 | dotColor: "#ef4444", |
apps/mobile/src/features/connection/connectionTone.ts+2-2| ... | ... | @@ -24,8 +24,8 @@ export function connectionTone(state: RemoteClientConnectionState): StatusTone { |
| 24 | 24 | case "unsupported": |
| 25 | 25 | return { |
| 26 | 26 | label: "Client not supported", |
| 27 | pillClassName: "bg-danger", | |
| 28 | textClassName: "text-danger-foreground", | |
| 27 | pillClassName: "bg-subtle", | |
| 28 | textClassName: "text-foreground-secondary", | |
| 29 | 29 | }; |
| 30 | 30 | case "error": |
| 31 | 31 | return { |
apps/web/src/components/ConnectionStatusDot.tsx-1| ... | ... | @@ -11,7 +11,6 @@ export function connectionPhaseDotClassName(phase: EnvironmentConnectionPhase): |
| 11 | 11 | case "connecting": |
| 12 | 12 | case "reconnecting": |
| 13 | 13 | return "bg-warning"; |
| 14 | case "unsupported": | |
| 15 | 14 | case "error": |
| 16 | 15 | return "bg-destructive"; |
| 17 | 16 | default: |
apps/web/src/components/cloud/CloudEnvironmentConnectList.tsx+68-23| ... | ... | @@ -9,7 +9,11 @@ import { |
| 9 | 9 | isAtomCommandInterrupted, |
| 10 | 10 | squashAtomCommandFailure, |
| 11 | 11 | } from "@t3tools/client-runtime/state/runtime"; |
| 12 | import type { EnvironmentId } from "@t3tools/contracts"; | |
| 12 | import { | |
| 13 | type EnvironmentId, | |
| 14 | resolveEnvironmentMachineKind, | |
| 15 | type ServerConfig, | |
| 16 | } from "@t3tools/contracts"; | |
| 13 | 17 | import type { |
| 14 | 18 | RelayClientEnvironmentRecord, |
| 15 | 19 | RelayEnvironmentStatusResponse, |
| ... | ... | @@ -23,6 +27,7 @@ import { relayEnvironmentDiscovery } from "~/state/relay"; |
| 23 | 27 | import { useRelayEnvironmentDiscovery } from "~/state/environments"; |
| 24 | 28 | import { useAtomCommand } from "~/state/use-atom-command"; |
| 25 | 29 | import { ConnectionStatusDot } from "../ConnectionStatusDot"; |
| 30 | import { EnvironmentMachineIcon } from "../EnvironmentMachineIcon"; | |
| 26 | 31 | import { ITEM_ROW_CLASSNAME, ITEM_ROW_INNER_CLASSNAME } from "../settings/itemRows"; |
| 27 | 32 | import { Checkbox } from "../ui/checkbox"; |
| 28 | 33 | import { Button } from "../ui/button"; |
| ... | ... | @@ -43,6 +48,8 @@ function discoveredCompatibilityError( |
| 43 | 48 | export interface SavedCloudEnvironmentConnection { |
| 44 | 49 | readonly environmentId: EnvironmentId; |
| 45 | 50 | readonly connection: EnvironmentConnectionPresentation; |
| 51 | /** Present once connected; carries the user's icon override. */ | |
| 52 | readonly serverConfig?: ServerConfig | null; | |
| 46 | 53 | } |
| 47 | 54 | |
| 48 | 55 | function RemoteEnvironmentRowsSkeleton() { |
| ... | ... | @@ -296,15 +303,25 @@ export function CloudEnvironmentConnectRows({ |
| 296 | 303 | const compatibilityError = discoveredCompatibilityError(status); |
| 297 | 304 | const unsupported = |
| 298 | 305 | compatibilityError !== null || savedEnvironment?.connection.phase === "unsupported"; |
| 306 | const unsupportedDetail = | |
| 307 | compatibilityError?.message ?? savedEnvironment?.connection.error ?? null; | |
| 299 | 308 | const savedConnection = unsupported |
| 300 | 309 | ? presentSavedCloudEnvironmentConnection({ |
| 301 | 310 | phase: "unsupported", |
| 302 | error: compatibilityError?.message ?? savedEnvironment?.connection.error ?? null, | |
| 311 | error: unsupportedDetail, | |
| 303 | 312 | traceId: null, |
| 304 | 313 | }) |
| 305 | 314 | : savedEnvironment |
| 306 | 315 | ? presentSavedCloudEnvironmentConnection(savedEnvironment.connection) |
| 307 | 316 | : null; |
| 317 | // A connected machine's own config (with the user's icon pick) wins. Before | |
| 318 | // that, the relay's health probe already carries the server's descriptor, so | |
| 319 | // a machine can wear its detected glyph before this device ever connects. | |
| 320 | const descriptor = status === undefined ? undefined : Option.getOrNull(status)?.descriptor; | |
| 321 | const machineKind = resolveEnvironmentMachineKind( | |
| 322 | savedEnvironment?.serverConfig ?? | |
| 323 | (descriptor === undefined ? null : { environment: descriptor }), | |
| 324 | ); | |
| 308 | 325 | const dotClassName = savedConnection |
| 309 | 326 | ? savedConnection.tone === "connected" |
| 310 | 327 | ? "bg-success" |
| ... | ... | @@ -320,16 +337,19 @@ export function CloudEnvironmentConnectRows({ |
| 320 | 337 | : availability === "checking" |
| 321 | 338 | ? "bg-warning" |
| 322 | 339 | : "bg-muted-foreground/35"; |
| 323 | const statusText = savedConnection | |
| 324 | ? savedConnection.statusText | |
| 325 | : availability === "online" | |
| 326 | ? "T3 Connect · Not added · Relay online" | |
| 327 | : availability === "offline" | |
| 328 | ? "T3 Connect · Not added · Relay offline" | |
| 329 | : availability === "checking" | |
| 330 | ? "T3 Connect · Not added · Checking relay status…" | |
| 331 | : (Option.getOrNull(error)?.message ?? | |
| 332 | "T3 Connect · Not added · Relay status unavailable"); | |
| 340 | const statusText = | |
| 341 | unsupported && !savedEnvironment | |
| 342 | ? "T3 Connect · Not added · Client not supported" | |
| 343 | : savedConnection | |
| 344 | ? savedConnection.statusText | |
| 345 | : availability === "online" | |
| 346 | ? "T3 Connect · Not added · Relay online" | |
| 347 | : availability === "offline" | |
| 348 | ? "T3 Connect · Not added · Relay offline" | |
| 349 | : availability === "checking" | |
| 350 | ? "T3 Connect · Not added · Checking relay status…" | |
| 351 | : (Option.getOrNull(error)?.message ?? | |
| 352 | "T3 Connect · Not added · Relay status unavailable"); | |
| 333 | 353 | if (selection) { |
| 334 | 354 | return ( |
| 335 | 355 | <label |
| ... | ... | @@ -348,6 +368,11 @@ export function CloudEnvironmentConnectRows({ |
| 348 | 368 | } |
| 349 | 369 | }} |
| 350 | 370 | /> |
| 371 | <EnvironmentMachineIcon | |
| 372 | aria-hidden | |
| 373 | kind={machineKind} | |
| 374 | className="size-4 shrink-0 text-muted-foreground" | |
| 375 | /> | |
| 351 | 376 | <span className="min-w-0 flex-1 truncate text-sm font-medium">{environment.label}</span> |
| 352 | 377 | <Tooltip> |
| 353 | 378 | <TooltipTrigger |
| ... | ... | @@ -368,7 +393,9 @@ export function CloudEnvironmentConnectRows({ |
| 368 | 393 | ? "Unavailable" |
| 369 | 394 | : "Checking…"))} |
| 370 | 395 | </TooltipTrigger> |
| 371 | <TooltipPopup className="max-w-80 break-words">{statusText}</TooltipPopup> | |
| 396 | <TooltipPopup className="max-w-80 break-words"> | |
| 397 | {unsupportedDetail ?? statusText} | |
| 398 | </TooltipPopup> | |
| 372 | 399 | </Tooltip> |
| 373 | 400 | </label> |
| 374 | 401 | ); |
| ... | ... | @@ -387,17 +414,24 @@ export function CloudEnvironmentConnectRows({ |
| 387 | 414 | : null |
| 388 | 415 | } |
| 389 | 416 | tooltipText={ |
| 390 | savedConnection | |
| 391 | ? savedConnection.statusText | |
| 392 | : availability === "online" | |
| 393 | ? "Relay online" | |
| 394 | : availability === "offline" | |
| 395 | ? "Relay offline" | |
| 396 | : availability === "checking" | |
| 397 | ? "Checking relay status" | |
| 398 | : (Option.getOrNull(error)?.message ?? "Relay status unavailable") | |
| 417 | unsupportedDetail !== null | |
| 418 | ? unsupportedDetail | |
| 419 | : savedConnection | |
| 420 | ? savedConnection.statusText | |
| 421 | : availability === "online" | |
| 422 | ? "Relay online" | |
| 423 | : availability === "offline" | |
| 424 | ? "Relay offline" | |
| 425 | : availability === "checking" | |
| 426 | ? "Checking relay status" | |
| 427 | : (Option.getOrNull(error)?.message ?? "Relay status unavailable") | |
| 399 | 428 | } |
| 400 | 429 | /> |
| 430 | <EnvironmentMachineIcon | |
| 431 | aria-hidden | |
| 432 | kind={machineKind} | |
| 433 | className="size-4 shrink-0 text-muted-foreground" | |
| 434 | /> | |
| 401 | 435 | <p className="truncate text-sm font-medium">{environment.label}</p> |
| 402 | 436 | </div> |
| 403 | 437 | <p |
| ... | ... | @@ -413,7 +447,18 @@ export function CloudEnvironmentConnectRows({ |
| 413 | 447 | {statusText} |
| 414 | 448 | </p> |
| 415 | 449 | </div> |
| 416 | {savedConnection ? ( | |
| 450 | {unsupported && !savedEnvironment ? ( | |
| 451 | <Tooltip> | |
| 452 | <TooltipTrigger render={<span className="inline-flex" tabIndex={0} />}> | |
| 453 | <Button size="sm" disabled> | |
| 454 | Add | |
| 455 | </Button> | |
| 456 | </TooltipTrigger> | |
| 457 | <TooltipPopup className="max-w-80 break-words"> | |
| 458 | {unsupportedDetail ?? "Client not supported"} | |
| 459 | </TooltipPopup> | |
| 460 | </Tooltip> | |
| 461 | ) : savedConnection ? ( | |
| 417 | 462 | <Button size="sm" variant="outline" disabled> |
| 418 | 463 | {savedConnection.buttonLabel} |
| 419 | 464 | </Button> |
apps/web/src/components/cloud/cloudEnvironmentConnectionPresentation.test.ts+1| ... | ... | @@ -40,6 +40,7 @@ describe("saved cloud environment connection presentation", () => { |
| 40 | 40 | |
| 41 | 41 | it.each([ |
| 42 | 42 | ["error", "Connection failed", "Connection failed. Reason: Access denied.", "error"], |
| 43 | ["unsupported", "Client not supported", "Client not supported", "idle"], | |
| 43 | 44 | ["offline", "Offline", "Offline", "idle"], |
| 44 | 45 | ["available", "Not connected", "Available", "idle"], |
| 45 | 46 | ] as const)( |
apps/web/src/components/cloud/cloudEnvironmentConnectionPresentation.ts+7-2| ... | ... | @@ -36,11 +36,16 @@ export function presentSavedCloudEnvironmentConnection( |
| 36 | 36 | statusText: connectionStatusText(connection), |
| 37 | 37 | tone: "connecting", |
| 38 | 38 | }; |
| 39 | // Not a failure: the machine is fine, this build just cannot talk to it. | |
| 39 | 40 | case "unsupported": |
| 41 | return { | |
| 42 | buttonLabel: "Client not supported", | |
| 43 | statusText: connectionStatusText(connection), | |
| 44 | tone: "idle", | |
| 45 | }; | |
| 40 | 46 | case "error": |
| 41 | 47 | return { |
| 42 | buttonLabel: | |
| 43 | connection.phase === "unsupported" ? "Client not supported" : "Connection failed", | |
| 48 | buttonLabel: "Connection failed", | |
| 44 | 49 | statusText: connectionStatusText(connection), |
| 45 | 50 | tone: "error", |
| 46 | 51 | }; |
apps/web/src/components/settings/ConnectionsSettings.tsx+20-3| ... | ... | @@ -161,6 +161,7 @@ import { |
| 161 | 161 | type EnvironmentPresentation, |
| 162 | 162 | useEnvironments, |
| 163 | 163 | usePrimaryEnvironment, |
| 164 | useRelayEnvironmentDiscovery, | |
| 164 | 165 | } from "~/state/environments"; |
| 165 | 166 | import { requestConfirmDialog } from "~/confirmDialog"; |
| 166 | 167 | import { useAtomCommand } from "../../state/use-atom-command"; |
| ... | ... | @@ -1457,8 +1458,9 @@ function savedBackendStatus(environment: EnvironmentPresentation): { |
| 1457 | 1458 | text: connection.error ? `Reconnecting: ${connection.error}` : "Reconnecting", |
| 1458 | 1459 | tone: "error", |
| 1459 | 1460 | }; |
| 1461 | // Not a failure: the machine is fine, this build just cannot talk to it. | |
| 1460 | 1462 | case "unsupported": |
| 1461 | return { text: "Client not supported", tone: "error" }; | |
| 1463 | return { text: "Client not supported", tone: "muted" }; | |
| 1462 | 1464 | case "error": |
| 1463 | 1465 | return { |
| 1464 | 1466 | text: connection.error ? `Connection failed: ${connection.error}` : "Connection failed", |
| ... | ... | @@ -1519,6 +1521,17 @@ function SavedBackendListRow({ |
| 1519 | 1521 | serverUpdateState.status === "running" && serverUpdateState.stage === "resuming"; |
| 1520 | 1522 | const status = savedBackendStatus(environment); |
| 1521 | 1523 | const serverVersion = environment.serverConfig?.environment.serverVersion ?? null; |
| 1524 | // A saved T3 Connect machine this device has never reached (unsupported, | |
| 1525 | // or not yet connected) still has a descriptor from relay discovery, so | |
| 1526 | // it can wear its detected glyph instead of the generic server. | |
| 1527 | const relayDiscovery = useRelayEnvironmentDiscovery(); | |
| 1528 | const discoveredDescriptor = Option.getOrNull( | |
| 1529 | relayDiscovery.environments.get(environmentId)?.status ?? Option.none(), | |
| 1530 | )?.descriptor; | |
| 1531 | const machineKind = resolveEnvironmentMachineKind( | |
| 1532 | environment.serverConfig ?? | |
| 1533 | (discoveredDescriptor === undefined ? null : { environment: discoveredDescriptor }), | |
| 1534 | ); | |
| 1522 | 1535 | const subtitleText = [ |
| 1523 | 1536 | environmentTransportLabel(environment), |
| 1524 | 1537 | resumingServerUpdate ? "Restarting" : status.text, |
| ... | ... | @@ -1537,7 +1550,7 @@ function SavedBackendListRow({ |
| 1537 | 1550 | |
| 1538 | 1551 | return ( |
| 1539 | 1552 | <EnvironmentRow |
| 1540 | kind={resolveEnvironmentMachineKind(environment.serverConfig)} | |
| 1553 | kind={machineKind} | |
| 1541 | 1554 | label={environment.label} |
| 1542 | 1555 | dimmed={!enabled} |
| 1543 | 1556 | subtitle={ |
| ... | ... | @@ -1558,7 +1571,11 @@ function SavedBackendListRow({ |
| 1558 | 1571 | {subtitleText} |
| 1559 | 1572 | </TooltipTrigger> |
| 1560 | 1573 | <TooltipPopup side="top" className="max-w-80 whitespace-pre-wrap leading-tight"> |
| 1561 | {enabled || unsupported ? connectionStatusText(environment.connection) : "Switched off"} | |
| 1574 | {unsupported && environment.connection.error | |
| 1575 | ? environment.connection.error | |
| 1576 | : enabled | |
| 1577 | ? connectionStatusText(environment.connection) | |
| 1578 | : "Switched off"} | |
| 1562 | 1579 | {versionMismatch |
| 1563 | 1580 | ? `\nUpdate available: ${versionMismatch.serverVersion} → ${versionMismatch.clientVersion}` |
| 1564 | 1581 | : ""} |
packages/contracts/src/server.test.ts+6| ... | ... | @@ -207,6 +207,12 @@ describe("resolveEnvironmentMachineKind", () => { |
| 207 | 207 | ).toBe("mac-mini"); |
| 208 | 208 | }); |
| 209 | 209 | |
| 210 | it("uses detection from a bare descriptor before connecting", () => { | |
| 211 | expect(resolveEnvironmentMachineKind({ environment: descriptor({ machine: "laptop" }) })).toBe( | |
| 212 | "laptop", | |
| 213 | ); | |
| 214 | }); | |
| 215 | ||
| 210 | 216 | it("falls back to a server for older servers and before connect", () => { |
| 211 | 217 | expect( |
| 212 | 218 | resolveEnvironmentMachineKind({ |
packages/contracts/src/server.ts+10-5| ... | ... | @@ -598,14 +598,19 @@ export type ServerConfig = typeof ServerConfig.Type; |
| 598 | 598 | |
| 599 | 599 | /** |
| 600 | 600 | * The machine an environment should be drawn as: the user's pick, else what |
| 601 | * the server detected, else a generic server. A null config (not connected | |
| 602 | * yet, or an older server) resolves to the same generic so rows never | |
| 603 | * flicker between glyphs. | |
| 601 | * the server detected, else a generic server. Settings only exist once | |
| 602 | * connected; a descriptor alone (relay discovery, before any connection) | |
| 603 | * still yields the detected kind. A null config (nothing known yet, or an | |
| 604 | * older server) resolves to the same generic so rows never flicker between | |
| 605 | * glyphs. | |
| 604 | 606 | */ |
| 605 | 607 | export function resolveEnvironmentMachineKind( |
| 606 | config: Pick<ServerConfig, "environment" | "settings"> | null, | |
| 608 | config: { | |
| 609 | readonly environment: Pick<ExecutionEnvironmentDescriptor, "platform">; | |
| 610 | readonly settings?: Pick<ServerSettings, "environmentIcon">; | |
| 611 | } | null, | |
| 607 | 612 | ): EnvironmentMachineKind { |
| 608 | return config?.settings.environmentIcon ?? config?.environment.platform.machine ?? "server"; | |
| 613 | return config?.settings?.environmentIcon ?? config?.environment.platform.machine ?? "server"; | |
| 609 | 614 | } |
| 610 | 615 | |
| 611 | 616 | const ServerUpsertKeybindingReplaceTarget = Schema.Struct({ |