From 7b43f21940f06cd18bea8d53ce78f24fd4551c39 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 06:02:33 +0200 Subject: [PATCH] fix(app): let Remote SSH connections authenticate to a password-protected daemon (#6081) Co-authored-by: royenheart --- .../components/add-remote-ssh-host-modal.tsx | 158 ++++++++++++++++-- packages/app/src/i18n/resources/ar.ts | 6 + packages/app/src/i18n/resources/en.ts | 6 + packages/app/src/i18n/resources/es.ts | 6 + packages/app/src/i18n/resources/fr.ts | 6 + packages/app/src/i18n/resources/ja.ts | 6 + packages/app/src/i18n/resources/ko.ts | 6 + packages/app/src/i18n/resources/pt-BR.ts | 6 + packages/app/src/i18n/resources/ru.ts | 6 + packages/app/src/i18n/resources/zh-CN.ts | 6 + packages/app/src/runtime/host-runtime.test.ts | 47 ++++++ packages/app/src/runtime/host-runtime.ts | 5 + public-docs/connectivity.md | 5 +- 13 files changed, 254 insertions(+), 15 deletions(-) diff --git a/packages/app/src/components/add-remote-ssh-host-modal.tsx b/packages/app/src/components/add-remote-ssh-host-modal.tsx index 2e3e369f39..c7e25bce64 100644 --- a/packages/app/src/components/add-remote-ssh-host-modal.tsx +++ b/packages/app/src/components/add-remote-ssh-host-modal.tsx @@ -1,13 +1,25 @@ import { useCallback, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Text, View } from "react-native"; +import { + Pressable, + Text, + View, + type PressableStateCallbackType, + type StyleProp, + type ViewStyle, +} from "react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; -import { Terminal } from "lucide-react-native"; +import { Eye, EyeOff, Terminal } from "lucide-react-native"; import { parseSshTransportUri } from "@getpaseo/protocol/ssh-transport"; import type { HostProfile } from "@/types/host-connection"; import { useHostMutations, useHosts } from "@/runtime/host-runtime"; import { Button } from "@/components/ui/button"; import { Field, FormTextInput } from "@/components/ui/form-field"; +import { + CONTROL_HEIGHTS, + createControlGeometry, + resolveControlInteractionStyles, +} from "@/components/ui/control-geometry"; import type { EditingTextInputHandle } from "@/components/ui/text-input"; import { useIsCompactFormFactor } from "@/constants/layout"; import { DaemonConnectionTestError } from "@/utils/test-daemon-connection"; @@ -15,18 +27,64 @@ import { AdaptiveModalSheet, type SheetHeader } from "./adaptive-modal-sheet"; const FLEX_ONE_STYLE = { flex: 1 } as const; const ThemedTerminal = withUnistyles(Terminal); +const ThemedEye = withUnistyles(Eye, (theme) => ({ color: theme.colors.foregroundMuted })); +const ThemedEyeOff = withUnistyles(EyeOff, (theme) => ({ color: theme.colors.foregroundMuted })); -const styles = StyleSheet.create((theme) => ({ - helper: { - color: theme.colors.foregroundMuted, - fontSize: theme.fontSize.sm, - }, - actions: { - flexDirection: "row", - gap: theme.spacing[3], - marginTop: theme.spacing[2], - }, -})); +const styles = StyleSheet.create((theme) => { + const geometry = createControlGeometry(theme); + return { + helper: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.sm, + }, + passwordRow: { + flexDirection: "row", + alignItems: "center", + alignSelf: "stretch", + width: "100%", + gap: theme.spacing[2], + }, + passwordInputWrap: { + flex: 1, + minWidth: 0, + }, + // Boxed adornment next to the field: same height and radius as the field + // chrome of the active form size, and the shared interaction phases. + iconButtonSm: { + minHeight: CONTROL_HEIGHTS.compact, + width: CONTROL_HEIGHTS.compact, + alignItems: "center", + justifyContent: "center", + borderRadius: theme.borderRadius.md, + backgroundColor: theme.colors.surface2, + }, + iconButtonMd: { + minHeight: CONTROL_HEIGHTS.field, + width: CONTROL_HEIGHTS.field, + alignItems: "center", + justifyContent: "center", + borderRadius: theme.borderRadius.lg, + backgroundColor: theme.colors.surface2, + }, + controlRest: { + ...geometry.controlRest, + }, + controlHover: { + ...geometry.controlHover, + }, + controlActive: { + ...geometry.controlActive, + }, + controlDisabled: { + ...geometry.controlDisabled, + }, + actions: { + flexDirection: "row", + gap: theme.spacing[3], + marginTop: theme.spacing[2], + }, + }; +}); export interface AddRemoteSshHostModalProps { visible: boolean; @@ -51,14 +109,20 @@ export function AddRemoteSshHostModal({ const isCompact = useIsCompactFormFactor(); const { probeAndUpsertRemoteSshConnection } = useHostMutations(); const targetRef = useRef(""); + const passwordRef = useRef(""); const inputRef = useRef(null); + const passwordInputRef = useRef(null); + const [isPasswordVisible, setIsPasswordVisible] = useState(false); const [isSaving, setIsSaving] = useState(false); const [errorMessage, setErrorMessage] = useState(""); const header = useMemo(() => ({ title: t("pairing.remoteSsh.title") }), [t]); const clear = useCallback(() => { targetRef.current = ""; + passwordRef.current = ""; inputRef.current?.replaceText(""); + passwordInputRef.current?.replaceText(""); + setIsPasswordVisible(false); setErrorMessage(""); }, []); @@ -94,7 +158,10 @@ export function AddRemoteSshHostModal({ try { setIsSaving(true); setErrorMessage(""); - result = await probeAndUpsertRemoteSshConnection(target); + result = await probeAndUpsertRemoteSshConnection({ + ...target, + password: passwordRef.current === "" ? undefined : passwordRef.current, + }); } catch (error) { const message = error instanceof DaemonConnectionTestError @@ -116,6 +183,34 @@ export function AddRemoteSshHostModal({ const handleTargetChange = useCallback((value: string) => { targetRef.current = value; }, []); + const handlePasswordChange = useCallback((value: string) => { + passwordRef.current = value; + }, []); + const handleTogglePasswordVisibility = useCallback(() => { + setIsPasswordVisible((currentlyVisible) => !currentlyVisible); + }, []); + const iconButtonStyle = useCallback( + ( + state: PressableStateCallbackType & { hovered?: boolean; focused?: boolean }, + ): StyleProp => [ + isCompact ? styles.iconButtonMd : styles.iconButtonSm, + resolveControlInteractionStyles( + { + controlRest: styles.controlRest, + controlHover: styles.controlHover, + controlActive: styles.controlActive, + controlDisabled: styles.controlDisabled, + }, + { + hovered: state.hovered, + pressed: state.pressed, + focused: state.focused, + disabled: isSaving, + }, + ), + ], + [isCompact, isSaving], + ); const handleSubmit = useCallback(() => void handleSave(), [handleSave]); return ( @@ -146,6 +241,41 @@ export function AddRemoteSshHostModal({ onSubmitEditing={handleSubmit} /> + + + + + + + {isPasswordVisible ? : } + + +