Skip to content
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3ac37aa
feat(connect): let hosts set their display name
PixPMusic Jul 29, 2026
cc3e495
fix(connect): gate host name edits on server readiness
PixPMusic Jul 29, 2026
86dc0f2
fix(server): keep environment labels consistent
PixPMusic Aug 3, 2026
21ae62c
fix(relay): order environment label updates
PixPMusic Aug 20, 2026
49a79b0
fix(connect): adapt host naming to current clients
PixPMusic Oct 13, 2026
ab40b7b
fix(client): show live host names throughout settings
PixPMusic Oct 13, 2026
a16d31f
fix(relay): retain ordering when a host name is cleared
PixPMusic Oct 13, 2026
dadae39
test(mobile): include descriptors in connection fixtures
PixPMusic Oct 13, 2026
6dec5a2
fix(connect): bind host label updates to the verified key
PixPMusic Oct 13, 2026
3af176d
feat(mobile): rename Connect hosts from the connection editor
PixPMusic Oct 13, 2026
c082ebf
fix(connect): order host labels using the relay clock
PixPMusic Oct 13, 2026
5ad8258
fix(mobile): name failed usage refreshes by the current host name
PixPMusic Oct 13, 2026
02f837e
feat(connect): push host name changes to the relay
PixPMusic Oct 13, 2026
3258ed0
feat(web): rename Connect hosts from Settings → Connections
PixPMusic Oct 13, 2026
4060f49
refactor(relay): stamp pushed host names in EnvironmentLinks
PixPMusic Oct 13, 2026
b737e08
Merge main into pixpmusic/connect-host-name
PixPMusic Oct 29, 2026
96f4bc4
fix(connect): adapt peer fixtures after merging main
PixPMusic Oct 29, 2026
6da6e95
fix(web): open host name search results in Access settings
PixPMusic Oct 29, 2026
2a5b48a
fix(server): read host names independently of provider secrets
PixPMusic Oct 29, 2026
aba3b65
fix(server): preserve host names through settings read failures
PixPMusic Oct 29, 2026
2682f73
fix: keep host names consistent across settings and clients
PixPMusic Oct 29, 2026
1dc8c89
fix: retain discovered host names and resend them on startup
PixPMusic Oct 29, 2026
c2f3091
fix: preserve current host names through discovery and usage views
PixPMusic Oct 29, 2026
de0f669
fix: recognize fresh discovery records when clearing host names
PixPMusic Oct 29, 2026
65c5f5a
test: preserve discovery record identity through refresh failures
PixPMusic Oct 29, 2026
e442168
fix: preserve host names through failed pushes and relinks
PixPMusic Oct 29, 2026
a257077
fix(relay): order host labels by observation time
PixPMusic Oct 29, 2026
cbb40ed
chore: merge main into connect host naming
maria-rcks Oct 31, 2026
4a2e6ae
fix(mobile): retain connection action accessibility
maria-rcks Oct 31, 2026
2b3ad19
chore: merge main into connect host naming
maria-rcks Nov 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type ConnectionDiagnosis,
connectionDiagnosisText,
connectionRoutes,
environmentDisplayLabel,
hasRelayRoute,
presentConnectionDiagnosis,
type EnvironmentPresentation,
Expand Down Expand Up @@ -37,7 +38,7 @@ export function useConnectionDiagnosis(
const relayOnly =
connectionRoutes(presentation.entry).length === 1 && hasRelayRoute(presentation.entry);
const diagnosis = presentConnectionDiagnosis({
hostLabel: presentation.entry.target.label,
hostLabel: environmentDisplayLabel(presentation.entry, presentation.serverConfig),
state,
via: relayOnly ? "relay" : "direct",
clientVersion: Constants.expoConfig?.version,
Expand Down
321 changes: 191 additions & 130 deletions apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { ConnectionTraceId } from "./ConnectionTraceId";
import { SymbolView } from "../../components/ui/AppSymbol";
import { hasSavedBearerRoute, hostFreezeNotice } from "@t3tools/client-runtime/connection";
import type { AtomCommandResult } from "@t3tools/client-runtime/state/runtime";
import { type EnvironmentId, resolveEnvironmentMachineKind } from "@t3tools/contracts";
import {
type EnvironmentId,
type ServerConfig,
resolveEnvironmentMachineKind,
} from "@t3tools/contracts";
import { useAtomValue } from "@effect/atom-react";
import * as Cause from "effect/Cause";
import { AsyncResult } from "effect/reactivity";
Expand All @@ -24,6 +28,7 @@ import { ConnectionDiagnosisDetails, useConnectionDiagnosis } from "./Connection
import { ConnectionFormField } from "./ConnectionFormField";
import { ConnectionStatusDot } from "./ConnectionStatusDot";
import { environmentStatusText } from "./environmentStatusText";
import { environmentCatalog } from "../../connection/catalog";

function connectionStatusLabel(environment: ConnectedEnvironmentSummary): string | null {
if (!environment.isEnabled && environment.connectionState !== "unsupported") {
Expand All @@ -36,7 +41,7 @@ function connectionStatusLabel(environment: ConnectedEnvironmentSummary): string
});
}

export function ConnectionEnvironmentRow(props: {
interface ConnectionEnvironmentRowProps {
readonly environment: ConnectedEnvironmentSummary;
readonly expanded: boolean;
readonly opensDetails?: boolean;
Expand All @@ -48,9 +53,12 @@ export function ConnectionEnvironmentRow(props: {
environmentId: EnvironmentId,
updates: { readonly label: string; readonly displayUrl: string },
) => Promise<AtomCommandResult<unknown, unknown>>;
}) {
const [label, setLabel] = useState(props.environment.environmentLabel);
const [url, setUrl] = useState(props.environment.displayUrl);
}

export function ConnectionEnvironmentRow(props: ConnectionEnvironmentRowProps) {
const catalog = useAtomValue(environmentCatalog.catalogValueAtom);
const isHostName =
catalog.entries.get(props.environment.environmentId)?.target._tag === "RelayConnectionTarget";
const serverConfig = useAtomValue(
serverEnvironment.configValueAtom(props.environment.environmentId),
);
Expand All @@ -74,21 +82,6 @@ export function ConnectionEnvironmentRow(props: {
enabled &&
(props.environment.connectionState === "connecting" ||
props.environment.connectionState === "reconnecting");
const handleSave = async () => {
const result = await props.onUpdate(props.environment.environmentId, {
label: label.trim(),
displayUrl: url.trim(),
});
if (AsyncResult.isSuccess(result)) {
props.onToggle();
return;
}
const error = Cause.squash(result.cause);
Dialog.alert(
"Could not update environment",
error instanceof Error ? error.message : "The environment could not be updated.",
);
};

return (
<Animated.View layout={LinearTransition.duration(250)} className="bg-grouped-card">
Expand Down Expand Up @@ -121,7 +114,7 @@ export function ConnectionEnvironmentRow(props: {
{props.environment.environmentLabel}
</Text>
</View>
{!props.environment.isRelayManaged && props.environment.displayUrl.trim() ? (
{!isHostName && props.environment.displayUrl.trim() ? (
<Text className="text-xs text-foreground-muted" numberOfLines={1}>
{props.environment.displayUrl}
</Text>
Expand Down Expand Up @@ -197,119 +190,187 @@ export function ConnectionEnvironmentRow(props: {
</View>

{props.expanded ? (
<Animated.View
entering={FadeIn.duration(200)}
exiting={FadeOut.duration(150)}
className="gap-3 px-4 pb-4"
>
{diagnosis ? (
<ConnectionDiagnosisDetails
hostLabel={props.environment.environmentLabel}
diagnosis={diagnosis}
/>
) : null}
{props.environment.isRelayManaged ? (
<Text className="text-sm text-foreground-muted">
Managed by T3 Connect. Tunnel details update automatically.
</Text>
) : (
<>
<ConnectionFormField
label="Label"
autoCapitalize="words"
autoCorrect={false}
placeholder="My MacBook"
value={label}
onChangeText={setLabel}
/>
<ConnectionEnvironmentEditor
key={isHostName ? "host" : "local"}
{...props}
isHostName={isHostName}
enabled={enabled}
serverConfig={serverConfig}
diagnosis={diagnosis}
/>
) : null}
</Animated.View>
);
}

<ConnectionFormField
label="URL"
autoCapitalize="none"
autoCorrect={false}
keyboardType="url"
placeholder="192.168.1.100:8080"
value={url}
onChangeText={setUrl}
/>
</>
)}
/** Mounted only while expanded, so each opening starts from the latest saved values. */
function ConnectionEnvironmentEditor(
props: ConnectionEnvironmentRowProps & {
readonly isHostName: boolean;
readonly enabled: boolean;
readonly serverConfig: ServerConfig | null;
readonly diagnosis: ReturnType<typeof useConnectionDiagnosis>["diagnosis"];
},
) {
const { isHostName, enabled, serverConfig, diagnosis } = props;
const [draft, setDraft] = useState<{ readonly label?: string; readonly url?: string }>({});
const [saving, setSaving] = useState(false);
const label =
draft.label ??
(isHostName
? (serverConfig?.settings.environmentLabel ?? "")
: props.environment.environmentLabel);
const url = draft.url ?? props.environment.displayUrl;
const canUpdateSettings = useAtomValue(
serverEnvironment.updateSettings.permissionAtom(props.environment.environmentId, {
patch: { environmentLabel: label.trim() },
}),
);
const canEditHost =
props.environment.connectionState === "connected" &&
serverConfig?.environment.capabilities.environmentLabel === true &&
canUpdateSettings;
const canSave = !saving && (!isHostName || canEditHost);
async function handleSave() {
if (!canSave) return;
setSaving(true);
const result = await props
.onUpdate(props.environment.environmentId, {
label: label.trim(),
displayUrl: url.trim(),
})
.finally(() => setSaving(false));
if (AsyncResult.isSuccess(result)) {
props.onToggle();
return;
}
const error = Cause.squash(result.cause);
Dialog.alert(
"Could not update environment",
error instanceof Error ? error.message : "The environment could not be updated.",
);
}

{Platform.OS === "android" ? (
<View className="flex-row items-center justify-end gap-2">
{props.environment.isRelayManaged ? null : (
<View className="flex-1">
<Button
label="Save"
tone="primary"
fullWidth
onPress={() => {
void handleSave();
}}
/>
</View>
)}
<IconButton
accessibilityLabel="Reconnect environment"
icon="arrow.clockwise"
variant="tonal"
disabled={!enabled}
onPress={() => props.onReconnect(props.environment.environmentId)}
/>
<IconButton
accessibilityLabel="Remove environment"
icon="trash"
variant="danger"
onPress={() => props.onRemove(props.environment.environmentId)}
/>
</View>
) : (
<View className="flex-row justify-end gap-2">
{props.environment.isRelayManaged ? null : (
<Pressable
className="min-h-[42px] flex-1 flex-row items-center justify-center gap-1.5 rounded-[14px] bg-primary px-3.5 py-2.5 active:opacity-70"
onPress={handleSave}
>
<SymbolView
name="checkmark"
size={13}
tintColorClassName="accent-primary-foreground"
type="monochrome"
/>
<Text className="text-xs font-t3-bold tracking-[0.8px] uppercase text-primary-foreground">
Save
</Text>
</Pressable>
)}
return (
<Animated.View
entering={FadeIn.duration(200)}
exiting={FadeOut.duration(150)}
className="gap-3 px-4 pb-4"
>
{diagnosis ? (
<ConnectionDiagnosisDetails
hostLabel={props.environment.environmentLabel}
diagnosis={diagnosis}
/>
) : null}
<ConnectionFormField
label={isHostName ? "Host name" : "Label"}
autoCapitalize="words"
autoCorrect={false}
placeholder="My MacBook"
value={label}
editable={!saving && (!isHostName || canEditHost)}
onChangeText={(value) => {
setDraft((previous) => ({ ...previous, label: value }));
}}
/>

<Pressable
className="h-[42px] w-[42px] items-center justify-center rounded-[14px] border border-input-border bg-input active:opacity-70 disabled:opacity-40"
disabled={!enabled}
onPress={() => props.onReconnect(props.environment.environmentId)}
>
<SymbolView
name="arrow.clockwise"
size={14}
tintColorClassName="accent-icon-subtle"
type="monochrome"
/>
</Pressable>
{isHostName ? (
<Text className="text-sm text-foreground-muted">
{props.environment.connectionState !== "connected"
? "Connect to this environment to change its host name."
: serverConfig?.environment.capabilities.environmentLabel !== true
? "Update the host server to change its name."
: !canUpdateSettings
? "You need permission to update this environment’s settings."
: "Shared with all connected clients. Leave empty to use the machine name."}
</Text>
) : (
<ConnectionFormField
label="URL"
autoCapitalize="none"
autoCorrect={false}
keyboardType="url"
placeholder="192.168.1.100:8080"
value={url}
editable={!saving}
onChangeText={(value) => {
setDraft((previous) => ({ ...previous, url: value }));
}}
/>
)}

<Pressable
className="h-[42px] w-[42px] items-center justify-center rounded-[14px] border border-danger-border bg-danger active:opacity-70"
onPress={() => props.onRemove(props.environment.environmentId)}
>
<SymbolView
name="trash"
size={14}
tintColorClassName="accent-danger-foreground"
type="monochrome"
/>
</Pressable>
</View>
)}
</Animated.View>
) : null}
{Platform.OS === "android" ? (
<View className="flex-row items-center justify-end gap-2">
<View className="flex-1">
<Button
label="Save"
tone="primary"
fullWidth
disabled={!canSave}
onPress={() => {
void handleSave();
}}
/>
</View>
<IconButton
accessibilityLabel="Reconnect environment"
icon="arrow.clockwise"
variant="tonal"
disabled={!enabled}
onPress={() => props.onReconnect(props.environment.environmentId)}
/>
<IconButton
accessibilityLabel="Remove environment"
icon="trash"
variant="danger"
onPress={() => props.onRemove(props.environment.environmentId)}
/>
</View>
) : (
<View className="flex-row justify-end gap-2">
<Pressable
className="min-h-[42px] flex-1 flex-row items-center justify-center gap-1.5 rounded-[14px] bg-primary px-3.5 py-2.5 active:opacity-70 disabled:opacity-40"
disabled={!canSave}
onPress={handleSave}
>
<SymbolView
name="checkmark"
size={13}
tintColorClassName="accent-primary-foreground"
type="monochrome"
/>
<Text className="text-xs font-t3-bold tracking-[0.8px] uppercase text-primary-foreground">
Save
</Text>
</Pressable>

<Pressable
className="h-[42px] w-[42px] items-center justify-center rounded-[14px] border border-input-border bg-input active:opacity-70 disabled:opacity-40"
disabled={!enabled}
onPress={() => props.onReconnect(props.environment.environmentId)}
>
<SymbolView
name="arrow.clockwise"
size={14}
tintColorClassName="accent-icon-subtle"
type="monochrome"
/>
</Pressable>

<Pressable
className="h-[42px] w-[42px] items-center justify-center rounded-[14px] border border-danger-border bg-danger active:opacity-70"
onPress={() => props.onRemove(props.environment.environmentId)}
>
<SymbolView
name="trash"
size={14}
tintColorClassName="accent-danger-foreground"
type="monochrome"
/>
</Pressable>
</View>
)}
</Animated.View>
);
}
Loading
Loading