diff --git a/packages/app/e2e/helpers/settings.ts b/packages/app/e2e/helpers/settings.ts index b57feaa67..adc13eb3c 100644 --- a/packages/app/e2e/helpers/settings.ts +++ b/packages/app/e2e/helpers/settings.ts @@ -156,7 +156,7 @@ export async function expectDiagnosticsContent(page: Page): Promise { } export async function expectAboutContent(page: Page): Promise { - await expect(page.getByText("Version", { exact: true }).first()).toBeVisible(); + await expect(page.getByText("App version", { exact: true }).first()).toBeVisible(); } export async function expectGeneralContent(page: Page): Promise { diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx index e7422c7da..21ab81bb3 100644 --- a/packages/app/src/screens/settings-screen.tsx +++ b/packages/app/src/screens/settings-screen.tsx @@ -45,7 +45,14 @@ import { type Settings as EffectiveSettings, } from "@/hooks/use-settings"; import { THEME_SWATCHES } from "@/styles/theme"; -import { getHostRuntimeStore, isHostRuntimeConnected, useHosts } from "@/runtime/host-runtime"; +import { + getHostRuntimeStore, + isHostRuntimeConnected, + useHostRuntimeIsConnected, + useHosts, +} from "@/runtime/host-runtime"; +import { useSessionStore } from "@/stores/session-store"; +import type { HostProfile } from "@/types/host-connection"; import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region"; import { useWindowControlsPadding } from "@/utils/desktop-window"; import { confirmDialog } from "@/utils/confirm-dialog"; @@ -442,29 +449,114 @@ function DiagnosticsSection({ } interface AboutSectionProps { + appVersion: string | null; appVersionText: string; isDesktopApp: boolean; } -function AboutSection({ appVersionText, isDesktopApp }: AboutSectionProps) { +function AboutSection({ appVersion, appVersionText, isDesktopApp }: AboutSectionProps) { return ( - - - - - Version + <> + + + + + App version + This device + + {appVersionText} - {appVersionText} + {isDesktopApp ? : null} - {isDesktopApp ? : null} - + + + + ); +} + +function normalizeVersion(version: string | null | undefined): string | null { + const trimmed = version?.trim(); + if (!trimmed) return null; + return trimmed.replace(/^v/i, ""); +} + +function ConnectedHostsSection({ clientVersion }: { clientVersion: string | null }) { + const hosts = useHosts(); + if (hosts.length === 0) { + return null; + } + return ( + + + {hosts.map((host, index) => ( + 0} + clientVersion={clientVersion} + /> + ))} + ); } +function HostVersionRow({ + host, + showBorder, + clientVersion, +}: { + host: HostProfile; + showBorder: boolean; + clientVersion: string | null; +}) { + const isConnected = useHostRuntimeIsConnected(host.serverId); + const daemonVersion = useSessionStore( + (state) => state.sessions[host.serverId]?.serverInfo?.version ?? null, + ); + + const rowStyle = useMemo( + () => [settingsStyles.row, showBorder && settingsStyles.rowBorder], + [showBorder], + ); + + const normalizedHost = normalizeVersion(daemonVersion); + const normalizedClient = normalizeVersion(clientVersion); + const isMismatch = + normalizedHost !== null && normalizedClient !== null && normalizedHost !== normalizedClient; + + let valueText: string; + if (!isConnected) { + valueText = "Offline"; + } else if (normalizedHost) { + valueText = formatVersionWithPrefix(normalizedHost); + } else { + valueText = "—"; + } + + const valueStyle = useMemo( + () => [styles.aboutValue, isMismatch && styles.aboutVersionMismatch], + [isMismatch], + ); + + return ( + + + + {host.label} + + {isMismatch ? ( + Version differs from this device + ) : null} + + {valueText} + + ); +} + function getUpdateButtonLabel( isInstalling: boolean, latestVersion: string | null | undefined, @@ -1106,7 +1198,13 @@ export default function SettingsScreen({ view }: SettingsScreenProps) { /> ); case "about": - return ; + return ( + + ); } } return null; @@ -1266,6 +1364,9 @@ const styles = StyleSheet.create((theme) => ({ color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, }, + aboutVersionMismatch: { + color: theme.colors.palette.amber[500], + }, aboutErrorText: { color: theme.colors.palette.red[300], fontSize: theme.fontSize.xs,