diff --git a/apps/web/src/components/settings/ConnectionsSettings.logic.test.ts b/apps/web/src/components/settings/ConnectionsSettings.logic.test.ts index 74283796a8e2..155ed740d257 100644 --- a/apps/web/src/components/settings/ConnectionsSettings.logic.test.ts +++ b/apps/web/src/components/settings/ConnectionsSettings.logic.test.ts @@ -2,6 +2,8 @@ import type { AdvertisedEndpoint, DesktopWslState } from "@t3tools/contracts"; import { describe, expect, it, vi } from "vite-plus/test"; import { applyWslEnableSelection, + describeAddEnvironmentProgress, + displayPairingHost, isQrShareableEndpoint, isWslSettingsRowVisible, selectQrEndpointOption, @@ -183,3 +185,58 @@ describe("selectQrEndpointOption", () => { expect(selectQrEndpointOption([], "anything", "anything")).toBeNull(); }); }); + +describe("describeAddEnvironmentProgress", () => { + const describeAt = (mode: "remote" | "ssh", elapsedMs: number) => + describeAddEnvironmentProgress({ mode, host: "devbox", elapsedMs }); + + it("formats elapsed time as m:ss, flooring partial seconds", () => { + expect(describeAt("ssh", 0).elapsedLabel).toBe("0:00"); + expect(describeAt("ssh", 4_000).elapsedLabel).toBe("0:04"); + expect(describeAt("ssh", 92_000).elapsedLabel).toBe("1:32"); + expect(describeAt("ssh", 725_000).elapsedLabel).toBe("12:05"); + expect(describeAt("ssh", 4_999).elapsedLabel).toBe("0:04"); + }); + + it("swaps the remote detail for the slow hint at the threshold", () => { + expect(describeAt("remote", 7_999).detail).toBe( + "Verifying the pairing code and saving the environment.", + ); + expect(describeAt("remote", 8_000).detail).toBe( + "Still waiting for the host. Check that it is reachable from this device.", + ); + }); + + it("swaps the SSH detail for the slow hint at the threshold", () => { + expect(describeAt("ssh", 7_999).detail).toBe( + "Starting the T3 Code server on the remote machine.", + ); + expect(describeAt("ssh", 8_000).detail).toBe( + "Still working. First-time setup installs T3 Code on the remote machine and can take a few minutes.", + ); + }); + + it("names the host being contacted in each mode", () => { + expect( + describeAddEnvironmentProgress({ mode: "remote", host: "backend.example.com", elapsedMs: 0 }) + .title, + ).toBe("Contacting backend.example.com…"); + expect( + describeAddEnvironmentProgress({ mode: "ssh", host: "devbox", elapsedMs: 0 }).title, + ).toBe("Connecting to devbox over SSH…"); + }); +}); + +describe("displayPairingHost", () => { + it("keeps a bare host or host:port as typed", () => { + expect(displayPairingHost("backend.example.com")).toBe("backend.example.com"); + expect(displayPairingHost(" 10.13.37.3:3773 ")).toBe("10.13.37.3:3773"); + }); + + it("drops the scheme, path, and pairing token from a URL", () => { + expect(displayPairingHost("https://backend.example.com/pair#token=ABC")).toBe( + "backend.example.com", + ); + expect(displayPairingHost("http://10.13.37.3:3773")).toBe("10.13.37.3:3773"); + }); +}); diff --git a/apps/web/src/components/settings/ConnectionsSettings.logic.ts b/apps/web/src/components/settings/ConnectionsSettings.logic.ts index d683efab3a4a..3ec94b2f0add 100644 --- a/apps/web/src/components/settings/ConnectionsSettings.logic.ts +++ b/apps/web/src/components/settings/ConnectionsSettings.logic.ts @@ -70,3 +70,49 @@ export async function applyWslEnableSelection(input: { } return await bridge.setWslBackendEnabled(true); } + +/** Sits under the 10s remote request timeout so the hint lands before a dead host errors out. */ +export const ADD_ENVIRONMENT_SLOW_HINT_MS = 8_000; + +export function describeAddEnvironmentProgress(input: { + readonly mode: "remote" | "ssh"; + readonly host: string; + readonly elapsedMs: number; +}): { readonly title: string; readonly detail: string; readonly elapsedLabel: string } { + const { mode, host, elapsedMs } = input; + const totalSeconds = Math.floor(elapsedMs / 1000); + const seconds = totalSeconds % 60; + const elapsedLabel = `${Math.floor(totalSeconds / 60)}:${String(seconds).padStart(2, "0")}`; + const isSlow = elapsedMs >= ADD_ENVIRONMENT_SLOW_HINT_MS; + + if (mode === "ssh") { + return { + title: `Connecting to ${host} over SSH…`, + detail: isSlow + ? "Still working. First-time setup installs T3 Code on the remote machine and can take a few minutes." + : "Starting the T3 Code server on the remote machine.", + elapsedLabel, + }; + } + return { + title: `Contacting ${host}…`, + detail: isSlow + ? "Still waiting for the host. Check that it is reachable from this device." + : "Verifying the pairing code and saving the environment.", + elapsedLabel, + }; +} + +/** The host a user typed, reduced to what identifies the server: no scheme, path, or pairing token. */ +export function displayPairingHost(input: string): string { + const raw = input.trim(); + for (const candidate of [raw, `http://${raw}`]) { + try { + const host = new URL(candidate).host; + if (host) return host; + } catch { + // fall through to the next candidate + } + } + return raw; +} diff --git a/apps/web/src/components/settings/ConnectionsSettings.tsx b/apps/web/src/components/settings/ConnectionsSettings.tsx index 5a6f3ebd70b0..ec94ff376a26 100644 --- a/apps/web/src/components/settings/ConnectionsSettings.tsx +++ b/apps/web/src/components/settings/ConnectionsSettings.tsx @@ -48,6 +48,8 @@ import { formatElapsedDurationLabel, formatExpiresInLabel } from "../../timestam import { resolveDesktopPairingUrl, resolveHostedPairingUrl } from "./pairingUrls"; import { applyWslEnableSelection, + describeAddEnvironmentProgress, + displayPairingHost, isQrShareableEndpoint, isWslSettingsRowVisible, selectQrEndpointOption, @@ -540,6 +542,40 @@ function endpointShareHint(endpoint: AdvertisedEndpoint, url: string): string { } } +type AddEnvironmentProgress = { + readonly mode: "remote" | "ssh"; + readonly host: string; + readonly startedAtMs: number; +}; + +const AddEnvironmentProgressPanel = memo(function AddEnvironmentProgressPanel({ + progress, +}: { + progress: AddEnvironmentProgress; +}) { + const nowMs = useRelativeTimeTick(1_000); + const { title, detail, elapsedLabel } = describeAddEnvironmentProgress({ + mode: progress.mode, + host: progress.host, + elapsedMs: Math.max(0, nowMs - progress.startedAtMs), + }); + return ( +
{detail}
+{savedBackendError}
: null} - + {addEnvironmentProgress ? ( +