diff --git a/packages/app/src/components/add-host-modal.tsx b/packages/app/src/components/add-host-modal.tsx
index 2a64ebd66..bd272fec2 100644
--- a/packages/app/src/components/add-host-modal.tsx
+++ b/packages/app/src/components/add-host-modal.tsx
@@ -218,6 +218,7 @@ export function AddHostModal({
const profile = await upsertDirectConnection({
serverId,
endpoint,
+ label: hostname ?? undefined,
});
onSaved?.({ profile, serverId, hostname, isNewHost });
diff --git a/packages/app/src/components/name-host-modal.tsx b/packages/app/src/components/name-host-modal.tsx
deleted file mode 100644
index 3eb831c3b..000000000
--- a/packages/app/src/components/name-host-modal.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import { useCallback, useEffect, useRef, useState } from "react";
-import { Text, View } from "react-native";
-import { StyleSheet, useUnistyles } from "react-native-unistyles";
-import { AdaptiveModalSheet, AdaptiveTextInput } from "./adaptive-modal-sheet";
-import { Button } from "@/components/ui/button";
-
-const styles = StyleSheet.create((theme) => ({
- helper: {
- color: theme.colors.foregroundMuted,
- fontSize: theme.fontSize.sm,
- },
- field: {
- marginTop: theme.spacing[3],
- gap: theme.spacing[2],
- },
- label: {
- color: theme.colors.foregroundMuted,
- fontSize: theme.fontSize.sm,
- fontWeight: theme.fontWeight.medium,
- },
- input: {
- backgroundColor: theme.colors.surface2,
- borderRadius: theme.borderRadius.lg,
- paddingHorizontal: theme.spacing[4],
- paddingVertical: theme.spacing[3],
- color: theme.colors.foreground,
- borderWidth: 1,
- borderColor: theme.colors.border,
- },
- actions: {
- flexDirection: "row",
- gap: theme.spacing[3],
- marginTop: theme.spacing[4],
- },
-}));
-
-export interface NameHostModalProps {
- visible: boolean;
- serverId: string;
- hostname: string | null;
- onSkip: () => void;
- onSave: (label: string) => void;
-}
-
-export function NameHostModal({ visible, serverId, hostname, onSkip, onSave }: NameHostModalProps) {
- const { theme } = useUnistyles();
-
- const [label, setLabel] = useState("");
- const hasEditedRef = useRef(false);
-
- const suggested = (hostname?.trim() || serverId).trim();
-
- useEffect(() => {
- if (!visible) return;
- setLabel(suggested);
- hasEditedRef.current = false;
- }, [suggested, visible]);
-
- useEffect(() => {
- if (!visible) return;
- if (hasEditedRef.current) return;
- if (!hostname) return;
- const trimmed = label.trim();
- if (trimmed.length === 0 || trimmed === serverId) {
- setLabel(hostname.trim());
- }
- }, [hostname, label, serverId, visible]);
-
- const handleChange = useCallback((value: string) => {
- hasEditedRef.current = true;
- setLabel(value);
- }, []);
-
- const handleSave = useCallback(() => {
- const trimmed = label.trim();
- if (!trimmed) {
- onSkip();
- return;
- }
- onSave(trimmed);
- }, [label, onSave, onSkip]);
-
- return (
-
- Optional. You can rename this later in Settings.
-
-
- Label
-
-
-
-
-
-
-
-
- );
-}
diff --git a/packages/app/src/components/pair-link-modal.tsx b/packages/app/src/components/pair-link-modal.tsx
index b0c3d30e8..5abd5a2ad 100644
--- a/packages/app/src/components/pair-link-modal.tsx
+++ b/packages/app/src/components/pair-link-modal.tsx
@@ -146,7 +146,7 @@ export function PairLinkModal({
await client.close().catch(() => undefined);
const isNewHost = !daemons.some((daemon) => daemon.serverId === parsedOffer.serverId);
- const profile = await upsertDaemonFromOfferUrl(raw);
+ const profile = await upsertDaemonFromOfferUrl(raw, hostname ?? undefined);
onSaved?.({ profile, serverId: parsedOffer.serverId, hostname, isNewHost });
handleClose();
} catch (error) {
diff --git a/packages/app/src/components/welcome-screen.tsx b/packages/app/src/components/welcome-screen.tsx
index 6f51ef1df..f88fef460 100644
--- a/packages/app/src/components/welcome-screen.tsx
+++ b/packages/app/src/components/welcome-screen.tsx
@@ -8,14 +8,11 @@ import type { HostProfile } from "@/types/host-connection";
import {
getHostRuntimeStore,
isHostRuntimeConnected,
- useHostMutations,
useHostRuntimeSnapshot,
useHosts,
} from "@/runtime/host-runtime";
-import { useSessionStore } from "@/stores/session-store";
import { AddHostModal } from "./add-host-modal";
import { PairLinkModal } from "./pair-link-modal";
-import { NameHostModal } from "./name-host-modal";
import { resolveAppVersion } from "@/utils/app-version";
import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates";
import { buildHostRootRoute } from "@/utils/host-routes";
@@ -237,41 +234,19 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
const router = useRouter();
- const { renameHost } = useHostMutations();
const appVersion = resolveAppVersion();
const appVersionText = formatVersionWithPrefix(appVersion);
const [isDirectOpen, setIsDirectOpen] = useState(false);
const [isPasteLinkOpen, setIsPasteLinkOpen] = useState(false);
- const [pendingNameHost, setPendingNameHost] = useState<{
- serverId: string;
- hostname: string | null;
- } | null>(null);
- const [pendingRedirectServerId, setPendingRedirectServerId] = useState(null);
const hosts = useHosts();
const anyOnlineServerId = useAnyHostOnline(hosts.map((h) => h.serverId));
- const pendingNameHostname = useSessionStore(
- useCallback(
- (state) => {
- if (!pendingNameHost) return null;
- return (
- state.sessions[pendingNameHost.serverId]?.serverInfo?.hostname ??
- pendingNameHost.hostname ??
- null
- );
- },
- [pendingNameHost],
- ),
- );
useEffect(() => {
if (!anyOnlineServerId) {
return;
}
- if (pendingNameHost) {
- return;
- }
router.replace(buildHostRootRoute(anyOnlineServerId));
- }, [anyOnlineServerId, pendingNameHost, router]);
+ }, [anyOnlineServerId, router]);
const finishOnboarding = useCallback(
(serverId: string) => {
@@ -396,13 +371,8 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
setIsDirectOpen(false)}
- onSaved={({ profile, serverId, hostname, isNewHost }) => {
+ onSaved={({ profile, serverId }) => {
onHostAdded?.(profile);
- setPendingRedirectServerId(serverId);
- if (isNewHost) {
- setPendingNameHost({ serverId, hostname });
- return;
- }
finishOnboarding(serverId);
}}
/>
@@ -410,38 +380,11 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
setIsPasteLinkOpen(false)}
- onSaved={({ profile, serverId, hostname, isNewHost }) => {
+ onSaved={({ profile, serverId }) => {
onHostAdded?.(profile);
- setPendingRedirectServerId(serverId);
- if (isNewHost) {
- setPendingNameHost({ serverId, hostname });
- return;
- }
finishOnboarding(serverId);
}}
/>
-
- {pendingNameHost && pendingRedirectServerId ? (
- {
- const serverId = pendingRedirectServerId;
- setPendingNameHost(null);
- setPendingRedirectServerId(null);
- finishOnboarding(serverId);
- }}
- onSave={(label) => {
- const serverId = pendingRedirectServerId;
- void renameHost(pendingNameHost.serverId, label).finally(() => {
- setPendingNameHost(null);
- setPendingRedirectServerId(null);
- finishOnboarding(serverId);
- });
- }}
- />
- ) : null}
);
}
diff --git a/packages/app/src/runtime/host-runtime.test.ts b/packages/app/src/runtime/host-runtime.test.ts
index dcebc6118..77aa1892e 100644
--- a/packages/app/src/runtime/host-runtime.test.ts
+++ b/packages/app/src/runtime/host-runtime.test.ts
@@ -5,6 +5,7 @@ import type {
FetchAgentsEntry,
FetchAgentsOptions,
} from "@server/client/daemon-client";
+import type { ConnectionOffer } from "@server/shared/connection-offer";
import type { HostConnection, HostProfile } from "@/types/host-connection";
import { useSessionStore, type Agent } from "@/stores/session-store";
import {
@@ -189,6 +190,17 @@ function makeHost(input?: Partial): HostProfile {
};
}
+function makeOffer(input?: Partial): ConnectionOffer {
+ return {
+ v: 2,
+ serverId: input?.serverId ?? "srv_offer",
+ daemonPublicKeyB64: input?.daemonPublicKeyB64 ?? "pk_test_offer",
+ relay: {
+ endpoint: input?.relay?.endpoint ?? "relay.paseo.sh:443",
+ },
+ };
+}
+
function makeDeps(
latencyByConnectionId: Record,
createdClients: FakeDaemonClient[],
@@ -1295,4 +1307,53 @@ describe("HostRuntimeStore", () => {
store.syncHosts([]);
});
+
+ it("uses the advertised hostname when adding a relay host from a pairing offer", async () => {
+ const store = new HostRuntimeStore({
+ deps: {
+ createClient: () => new FakeDaemonClient() as unknown as DaemonClient,
+ connectToDaemon: async ({ host }) => ({
+ client: makeConnectedProbeClient(5) as unknown as DaemonClient,
+ serverId: host.serverId,
+ hostname: host.label ?? null,
+ }),
+ getClientId: async () => "cid_test_runtime",
+ },
+ });
+
+ await store.upsertConnectionFromOffer(makeOffer(), "mbp");
+
+ const pairedHost = store.getHosts().find((host) => host.serverId === "srv_offer");
+ expect(pairedHost?.label).toBe("mbp");
+
+ store.syncHosts([]);
+ });
+
+ it("keeps a custom host label when re-pairing with an advertised hostname", async () => {
+ const store = new HostRuntimeStore({
+ deps: {
+ createClient: () => new FakeDaemonClient() as unknown as DaemonClient,
+ connectToDaemon: async ({ host }) => ({
+ client: makeConnectedProbeClient(5) as unknown as DaemonClient,
+ serverId: host.serverId,
+ hostname: host.label ?? null,
+ }),
+ getClientId: async () => "cid_test_runtime",
+ },
+ });
+
+ await store.upsertRelayConnection({
+ serverId: "srv_offer",
+ relayEndpoint: "relay.paseo.sh:443",
+ daemonPublicKeyB64: "pk_test_offer",
+ label: "Custom name",
+ });
+
+ await store.upsertConnectionFromOffer(makeOffer(), "mbp");
+
+ const pairedHost = store.getHosts().find((host) => host.serverId === "srv_offer");
+ expect(pairedHost?.label).toBe("Custom name");
+
+ store.syncHosts([]);
+ });
});
diff --git a/packages/app/src/runtime/host-runtime.ts b/packages/app/src/runtime/host-runtime.ts
index 2ad3648e3..864d69068 100644
--- a/packages/app/src/runtime/host-runtime.ts
+++ b/packages/app/src/runtime/host-runtime.ts
@@ -1287,15 +1287,22 @@ export class HostRuntimeStore {
});
}
- async upsertConnectionFromOffer(offer: ConnectionOffer): Promise {
+ async upsertConnectionFromOffer(
+ offer: ConnectionOffer,
+ label?: string,
+ ): Promise {
return this.upsertRelayConnection({
serverId: offer.serverId,
relayEndpoint: offer.relay.endpoint,
daemonPublicKeyB64: offer.daemonPublicKeyB64,
+ label,
});
}
- async upsertConnectionFromOfferUrl(offerUrlOrFragment: string): Promise {
+ async upsertConnectionFromOfferUrl(
+ offerUrlOrFragment: string,
+ label?: string,
+ ): Promise {
const marker = "#offer=";
const idx = offerUrlOrFragment.indexOf(marker);
if (idx === -1) {
@@ -1307,7 +1314,7 @@ export class HostRuntimeStore {
}
const payload = decodeOfferFragmentPayload(encoded);
const offer = ConnectionOfferSchema.parse(payload);
- return this.upsertConnectionFromOffer(offer);
+ return this.upsertConnectionFromOffer(offer, label);
}
async addConnectionFromListenAndWaitForOnline(input: {
@@ -1956,8 +1963,11 @@ export interface HostMutations {
daemonPublicKeyB64: string;
label?: string;
}) => Promise;
- upsertConnectionFromOffer: (offer: ConnectionOffer) => Promise;
- upsertConnectionFromOfferUrl: (offerUrlOrFragment: string) => Promise;
+ upsertConnectionFromOffer: (offer: ConnectionOffer, label?: string) => Promise;
+ upsertConnectionFromOfferUrl: (
+ offerUrlOrFragment: string,
+ label?: string,
+ ) => Promise;
renameHost: (serverId: string, label: string) => Promise;
removeHost: (serverId: string) => Promise;
removeConnection: (serverId: string, connectionId: string) => Promise;
@@ -1969,8 +1979,8 @@ export function useHostMutations(): HostMutations {
() => ({
upsertDirectConnection: (input) => store.upsertDirectConnection(input),
upsertRelayConnection: (input) => store.upsertRelayConnection(input),
- upsertConnectionFromOffer: (offer) => store.upsertConnectionFromOffer(offer),
- upsertConnectionFromOfferUrl: (url) => store.upsertConnectionFromOfferUrl(url),
+ upsertConnectionFromOffer: (offer, label) => store.upsertConnectionFromOffer(offer, label),
+ upsertConnectionFromOfferUrl: (url, label) => store.upsertConnectionFromOfferUrl(url, label),
renameHost: (serverId, label) => store.renameHost(serverId, label),
removeHost: (serverId) => store.removeHost(serverId),
removeConnection: (serverId, connectionId) => store.removeConnection(serverId, connectionId),
diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx
index 7287f5ed4..f6e7d2b7a 100644
--- a/packages/app/src/screens/settings-screen.tsx
+++ b/packages/app/src/screens/settings-screen.tsx
@@ -41,7 +41,6 @@ import { AddHostMethodModal } from "@/components/add-host-method-modal";
import { AddHostModal } from "@/components/add-host-modal";
import { PairLinkModal } from "@/components/pair-link-modal";
import { KeyboardShortcutsSection } from "@/screens/settings/keyboard-shortcuts-section";
-import { NameHostModal } from "@/components/name-host-modal";
import { Button } from "@/components/ui/button";
import { SegmentedControl } from "@/components/ui/segmented-control";
import {
@@ -96,20 +95,20 @@ function getSettingsSections(context: { isDesktopApp: boolean }): SettingsSectio
const sections: SettingsSectionDef[] = [
{ id: "hosts", label: "Hosts", icon: Server },
{ id: "general", label: "General", icon: Settings },
- { id: "shortcuts", label: "Shortcuts", icon: Keyboard },
{ id: "permissions", label: "Permissions", icon: Shield },
];
if (context.isDesktopApp) {
sections.push(
+ { id: "shortcuts", label: "Shortcuts", icon: Keyboard },
{ id: "integrations", label: "Integrations", icon: Puzzle },
{ id: "pair-device", label: "Pair device", icon: Smartphone },
{ id: "daemon", label: "Daemon", icon: Settings },
+ { id: "providers", label: "Providers", icon: Blocks },
);
}
sections.push(
- { id: "providers", label: "Providers", icon: Blocks },
{ id: "diagnostics", label: "Diagnostics", icon: Stethoscope },
{ id: "about", label: "About", icon: Info },
);
@@ -203,10 +202,6 @@ interface HostsSectionProps {
goBackToAddConnectionMethods: () => void;
setIsDirectHostVisible: (visible: boolean) => void;
setIsPasteLinkVisible: (visible: boolean) => void;
- pendingNameHost: { serverId: string; hostname: string | null } | null;
- setPendingNameHost: (host: { serverId: string; hostname: string | null } | null) => void;
- pendingNameHostname: string | null;
- renameHost: (serverId: string, label: string) => Promise;
pendingRemoveHost: HostProfile | null;
setPendingRemoveHost: (host: HostProfile | null) => void;
isRemovingHost: boolean;
@@ -289,38 +284,14 @@ function HostsSection(props: HostsSectionProps) {
visible={props.isDirectHostVisible}
onClose={props.closeAddConnectionFlow}
onCancel={props.goBackToAddConnectionMethods}
- onSaved={({ serverId, hostname, isNewHost }) => {
- if (isNewHost) {
- props.setPendingNameHost({ serverId, hostname });
- }
- }}
/>
{
- if (isNewHost) {
- props.setPendingNameHost({ serverId, hostname });
- }
- }}
/>
- {props.pendingNameHost ? (
- props.setPendingNameHost(null)}
- onSave={(label) => {
- void props.renameHost(props.pendingNameHost!.serverId, label).finally(() => {
- props.setPendingNameHost(null);
- });
- }}
- />
- ) : null}
-
{props.pendingRemoveHost ? (
(null);
const [pendingRemoveHost, setPendingRemoveHost] = useState(null);
const [isRemovingHost, setIsRemovingHost] = useState(false);
const [editingDaemon, setEditingDaemon] = useState(null);
@@ -905,19 +872,6 @@ export default function SettingsScreen() {
const editingDaemonLive = editingServerId
? (daemons.find((daemon) => daemon.serverId === editingServerId) ?? null)
: null;
- const pendingNameHostname = useSessionStore(
- useCallback(
- (state) => {
- if (!pendingNameHost) return null;
- return (
- state.sessions[pendingNameHost.serverId]?.serverInfo?.hostname ??
- pendingNameHost.hostname ??
- null
- );
- },
- [pendingNameHost],
- ),
- );
useEffect(() => {
return () => {
@@ -1077,10 +1031,6 @@ export default function SettingsScreen() {
goBackToAddConnectionMethods,
setIsDirectHostVisible,
setIsPasteLinkVisible,
- pendingNameHost,
- setPendingNameHost,
- pendingNameHostname,
- renameHost,
pendingRemoveHost,
setPendingRemoveHost,
isRemovingHost,