FN-006: label Cloudflare tunnel URLs accurately

Label remote access URLs with terminology that matches the active tunnel provider.

- derive share URL labels from the running remote provider
- add Cloudflare and neutral tunnel label translations and generated i18n types
- cover Cloudflare, Tailscale, and unknown-provider URL labels
- add a patch changeset for the corrected remote-access terminology

Files changed:
 .changeset/fn-006-remote-url-label.md              |  7 ++++++
 .../SettingsModal.remote-notifications.test.tsx    | 28 ++++++++++++++++++++--
 .../components/settings/sections/RemoteSection.tsx | 21 +++++++++++-----
 packages/i18n/locales/en/app.json                  |  2 ++
 packages/i18n/locales/es/app.json                  |  4 +++-
 packages/i18n/locales/fr/app.json                  |  4 +++-
 packages/i18n/locales/ko/app.json                  |  4 +++-
 packages/i18n/locales/pt-BR/app.json               |  4 +++-
 packages/i18n/locales/zh-CN/app.json               |  4 +++-
 packages/i18n/locales/zh-TW/app.json               |  4 +++-
 packages/i18n/src/resources.d.ts                   | 21 +++++++++++++---
 11 files changed, 86 insertions(+), 17 deletions(-)

Fusion-Task-Id: FN-006

Fusion-Task-Lineage: a34a3c77-11ab-4f73-a264-9776e75edfae

Co-authored-by: Fusion <noreply@runfusion.ai>
This commit is contained in:
Fusion Agent
2026-08-18 07:26:27 +00:00
parent cc6f389e71
commit d45c80d7f3
11 changed files with 86 additions and 17 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Remote Access now labels a Cloudflare tunnel URL correctly instead of calling it a Tailnet URL.
category: fix
dev: RemoteSection derives the share-block label from remoteStatus.provider; adds settings.remote.cloudflareTunnelURL and settings.remote.tunnelURL.

View File

@@ -517,14 +517,38 @@ describe("SettingsModal", () => {
expect(document.querySelector(".remote-share-block")).not.toBeInTheDocument();
});
it("renders remote-share-block when tunnel is running with a URL", async () => {
it("labels a running Cloudflare tunnel URL with Cloudflare terminology", async () => {
mockFetchRemoteStatus.mockResolvedValue({ provider: "cloudflare", state: "running", url: "https://demo-tunnel.trycloudflare.com/", lastError: null });
await renderModalSection("remote", "Remote Access");
const shareBlock = document.querySelector(".remote-share-block");
expect(shareBlock).toBeInTheDocument();
expect(shareBlock?.textContent).toContain("Cloudflare tunnel URL:");
expect(shareBlock?.textContent).toContain("https://demo-tunnel.trycloudflare.com/");
expect(shareBlock?.textContent).not.toContain("Tailnet");
});
it("retains the Tailnet URL label for a running Tailscale tunnel", async () => {
mockFetchRemoteStatus.mockResolvedValue({ provider: "tailscale", state: "running", url: "https://machine.ts.net/", lastError: null });
await renderModalSection("remote", "Remote Access");
const statusBar = document.querySelector(".remote-status-bar");
const shareBlock = document.querySelector(".remote-share-block");
expect(statusBar).toBeInTheDocument();
expect(statusBar?.className).toContain("remote-status-bar--running");
expect(document.querySelector(".remote-share-block")).toBeInTheDocument();
expect(shareBlock?.textContent).toContain("Tailnet URL:");
expect(shareBlock?.textContent).toContain("https://machine.ts.net/");
});
it("uses a neutral URL label when the running tunnel provider is unknown", async () => {
mockFetchRemoteStatus.mockResolvedValue({ provider: null, state: "running", url: "https://unknown.example/", lastError: null });
await renderModalSection("remote", "Remote Access");
const shareBlock = document.querySelector(".remote-share-block");
expect(shareBlock).toBeInTheDocument();
expect(shareBlock?.textContent).toContain("Tunnel URL:");
expect(shareBlock?.textContent).toContain("https://unknown.example/");
expect(shareBlock?.textContent).not.toContain("Tailnet");
});
it("updates provider selection via radio and shows provider status", async () => {

View File

@@ -122,22 +122,31 @@ export function RemoteSection({ form, setForm, remote }: RemoteSectionProps) {
</div>)}
{tunnelState === "running" && (remoteStatus?.url || tunnelShareLink) && (() => {
let accessCode: string | null = null;
let tailnetUrl: string | null = remoteStatus?.url ?? null;
let shareUrl: string | null = remoteStatus?.url ?? null;
if (tunnelShareLink?.url) {
try {
const parsed = new URL(tunnelShareLink.url);
accessCode = parsed.searchParams.get("rt");
if (!tailnetUrl)
tailnetUrl = `${parsed.origin}/`;
if (!shareUrl)
shareUrl = `${parsed.origin}/`;
}
catch {
// fall through
}
}
/*
FNXC:RemoteAccess 2026-08-18-07:10:
The share-block URL label must name the provider that is actually running (`remoteStatus.provider`), not the radio selection in `form` — the radio can hold an unsaved provider while a tunnel from the other provider is still up. A Cloudflare tunnel URL was previously labelled "Tailnet URL:", applying Tailscale vocabulary to a trycloudflare.com address. Unknown/null provider falls back to neutral "Tunnel URL:" so the row never asserts a provider it cannot prove.
*/
const shareUrlLabel = remoteStatus?.provider === "cloudflare"
? t("settings.remote.cloudflareTunnelURL", "Cloudflare tunnel URL:")
: remoteStatus?.provider === "tailscale"
? t("settings.remote.tailnetURL", "Tailnet URL:")
: t("settings.remote.tunnelURL", "Tunnel URL:");
return (<div className="remote-share-block">
{tailnetUrl && (<div className="remote-share-row">
<small>{t("settings.remote.tailnetURL", "Tailnet URL:")}</small>
<code className="settings-url-output">{tailnetUrl}</code>
{shareUrl && (<div className="remote-share-row">
<small>{shareUrlLabel}</small>
<code className="settings-url-output">{shareUrl}</code>
</div>)}
{accessCode && (<div className="remote-share-row">
<small>{t("settings.remote.remoteAccessCode", "Remote access code:")}</small>

View File

@@ -6656,6 +6656,7 @@
"authLinkTokenType": "Auth link token type",
"automaticallyRestoreTunnelOnStartupIfItWas": "Automatically restore tunnel on startup if it was running when last stopped. Default: disabled.",
"cloudflare": "Cloudflare",
"cloudflareTunnelURL": "Cloudflare tunnel URL:",
"cloudflaredInstalled": "cloudflared installed successfully",
"cloudflaredIsInstalled": "cloudflared is installed",
"cloudflaredIsNotInstalled": "cloudflared is not installed",
@@ -6709,6 +6710,7 @@
"tunnelStarted": "Remote tunnel started",
"tunnelStopped": "Remote tunnel stopped",
"tunnelToken": "Tunnel token",
"tunnelURL": "Tunnel URL:",
"uRLAndQRGenerationUseTheSelectedToken": " URL and QR generation use the selected token type. ",
"uRLNoHostnameOrPortConfigurationNeeded": " URL — no hostname or port configuration needed.",
"useExisting": "Use Existing",

View File

@@ -6687,7 +6687,9 @@
"installationFailed": "",
"acceptRoutesHint": "",
"shortLivedEnabledHint": "",
"shortLivedTtlMsHint": ""
"shortLivedTtlMsHint": "",
"cloudflareTunnelURL": "",
"tunnelURL": ""
},
"researchGlobal": {
"advancedExternalSearchProviders": "",

View File

@@ -6687,7 +6687,9 @@
"installationFailed": "",
"acceptRoutesHint": "",
"shortLivedEnabledHint": "",
"shortLivedTtlMsHint": ""
"shortLivedTtlMsHint": "",
"cloudflareTunnelURL": "",
"tunnelURL": ""
},
"researchGlobal": {
"advancedExternalSearchProviders": "",

View File

@@ -6687,7 +6687,9 @@
"installationFailed": "",
"acceptRoutesHint": "",
"shortLivedEnabledHint": "",
"shortLivedTtlMsHint": ""
"shortLivedTtlMsHint": "",
"cloudflareTunnelURL": "",
"tunnelURL": ""
},
"researchGlobal": {
"advancedExternalSearchProviders": "",

View File

@@ -6363,7 +6363,9 @@
"installationFailed": "Falha na instalação",
"acceptRoutesHint": "Padrão: desativado.",
"shortLivedEnabledHint": "Padrão: desativado.",
"shortLivedTtlMsHint": "Padrão: 900000 (15 minutos)."
"shortLivedTtlMsHint": "Padrão: 900000 (15 minutos).",
"cloudflareTunnelURL": "",
"tunnelURL": ""
},
"worktrees": {
"allowSilentSiblingBranchRenameDuringExecutorConflicts": " Permitir renomeação silenciosa de branch irmã durante conflitos do executor ",

View File

@@ -6687,7 +6687,9 @@
"installationFailed": "",
"acceptRoutesHint": "",
"shortLivedEnabledHint": "",
"shortLivedTtlMsHint": ""
"shortLivedTtlMsHint": "",
"cloudflareTunnelURL": "",
"tunnelURL": ""
},
"researchGlobal": {
"advancedExternalSearchProviders": "",

View File

@@ -6687,7 +6687,9 @@
"installationFailed": "",
"acceptRoutesHint": "",
"shortLivedEnabledHint": "",
"shortLivedTtlMsHint": ""
"shortLivedTtlMsHint": "",
"cloudflareTunnelURL": "",
"tunnelURL": ""
},
"researchGlobal": {
"advancedExternalSearchProviders": "",

View File

@@ -1029,15 +1029,15 @@ export default interface Resources {
"setupModeAriaLabel": "Agent setup mode",
"showSystemAgents": "Show system agents",
"skills": "Skills",
"skillsDescription": "Manage the skills available to this agent.",
"skillsDescription": "All enabled skills can be consulted automatically by any agent. Select skills below to force this agent to read them before starting work.",
"skillsErrors_one": "{{count}} skill{{plural}} error{{pluralError}}",
"skillsErrors_other": "{{count}} skill{{plural}} error{{pluralError}}",
"skillsFound_one": "{{count}} skill{{plural}} found",
"skillsFound_other": "{{count}} skill{{plural}} found",
"skillsHint": "Optional skills to assign to this agent",
"skillsHint": "All enabled skills can be consulted automatically by any agent. Select skills below to force this agent to read them before starting work.",
"skillsImported_one": "{{count}} skill{{plural}} imported",
"skillsImported_other": "{{count}} skill{{plural}} imported",
"skillsNone": "No skills assigned",
"skillsNone": "None",
"skillsSelected_one": "{{count}} skill selected",
"skillsSelected_other": "{{count}} skills selected",
"skillsSkipped_one": "{{count}} skill{{plural}} skipped (already exist)",
@@ -6690,6 +6690,7 @@ export default interface Resources {
"authenticatedURL": "Authenticated URL:",
"automaticallyRestoreTunnelOnStartupIfItWas": "Automatically restore tunnel on startup if it was running when last stopped. Default: disabled.",
"cloudflare": "Cloudflare",
"cloudflareTunnelURL": "Cloudflare tunnel URL:",
"cloudflaredInstalled": "cloudflared installed successfully",
"cloudflaredIsInstalled": "cloudflared is installed",
"cloudflaredIsNotInstalled": "cloudflared is not installed",
@@ -6746,6 +6747,7 @@ export default interface Resources {
"tunnelStarted": "Remote tunnel started",
"tunnelStopped": "Remote tunnel stopped",
"tunnelToken": "Tunnel token",
"tunnelURL": "Tunnel URL:",
"uRLAndQRGenerationUseTheSelectedToken": " URL and QR generation use the selected token type. ",
"uRLNoHostnameOrPortConfigurationNeeded": " URL — no hostname or port configuration needed.",
"useExisting": "Use Existing",
@@ -7435,12 +7437,16 @@ export default interface Resources {
"skills": {
"addSkill": "Add a skill…",
"allSkillsSelected": "All skills selected",
"autoAvailable": "Auto-available",
"autoAvailableTitle": "Enabled skills are available automatically.",
"catalogSection": "Skills Catalog",
"catalogUnavailable": "Catalog is temporarily unavailable. Please try again later.",
"closeDetail": "Close skill detail",
"closeView": "Close skills view",
"disableSkill": "Disable {{name}}",
"disabled": "Skill disabled",
"disabledSkill": "Disabled",
"disabledSkillTitle": "Disabled skills are not delivered to sessions, even when forced.",
"discovered": "discovered",
"discoveredCount_one": "{{count}} discovered skills",
"discoveredCount_other": "{{count}} discovered skills",
@@ -7448,6 +7454,9 @@ export default interface Resources {
"enableSkill": "Enable {{name}}",
"enabled": "Skill enabled",
"filesLabel": "Files",
"filter": "Filter skills",
"forced": "Forced",
"hiddenUnavailable": "Hidden skills include disabled or not-discovered entries",
"install": "Install",
"installError": "Failed to install skill",
"installFailed": "Failed to install {{name}}: {{message}}",
@@ -7458,19 +7467,25 @@ export default interface Resources {
"loadCatalogError": "Failed to load catalog",
"loadContentError": "Failed to load skill content",
"loadDiscoveredError": "Failed to load discovered skills",
"loadError": "Skills could not be loaded.",
"loading": "Loading skills…",
"loadingCatalog": "Loading catalog...",
"loadingContent": "Loading skill content...",
"loadingDiscovered": "Loading discovered skills...",
"noCatalogAvailable": "No skills available in the catalog.",
"noDiscovered": "No skills discovered in this project.",
"noMatches": "No matching skills",
"noMatchingDiscovered": "No discovered skills match your search.",
"noMatchingSearch": "No skills match your search.",
"noSkillMd": "(No SKILL.md found)",
"noSkillsDiscovered": "No skills discovered",
"notDiscovered": "Not discovered",
"notDiscoveredTitle": "This stored skill is no longer discovered by the project.",
"removeSkill": "Remove {{name}}",
"searchLabel": "Search skills",
"searchPlaceholder": "Search skills...",
"skillStatePending": "Checking availability",
"skillStatePendingTitle": "Skill discovery is loading or could not be refreshed.",
"title": "Skills",
"toggleError": "Failed to toggle skill",
"toggleFailed": "Failed to toggle skill: {{message}}",