diff --git a/.changeset/fn-006-remote-url-label.md b/.changeset/fn-006-remote-url-label.md new file mode 100644 index 0000000000..53d84b205b --- /dev/null +++ b/.changeset/fn-006-remote-url-label.md @@ -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. diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.remote-notifications.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.remote-notifications.test.tsx index dacba55a00..73a8613a55 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.remote-notifications.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.remote-notifications.test.tsx @@ -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 () => { diff --git a/packages/dashboard/app/components/settings/sections/RemoteSection.tsx b/packages/dashboard/app/components/settings/sections/RemoteSection.tsx index 3125ab3008..fbfeee28b7 100644 --- a/packages/dashboard/app/components/settings/sections/RemoteSection.tsx +++ b/packages/dashboard/app/components/settings/sections/RemoteSection.tsx @@ -122,22 +122,31 @@ export function RemoteSection({ form, setForm, remote }: RemoteSectionProps) { )} {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 (
- {tailnetUrl && (
- {t("settings.remote.tailnetURL", "Tailnet URL:")} - {tailnetUrl} + {shareUrl && (
+ {shareUrlLabel} + {shareUrl}
)} {accessCode && (
{t("settings.remote.remoteAccessCode", "Remote access code:")} diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json index b721699fba..2cffc9b0ad 100644 --- a/packages/i18n/locales/en/app.json +++ b/packages/i18n/locales/en/app.json @@ -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", diff --git a/packages/i18n/locales/es/app.json b/packages/i18n/locales/es/app.json index ea2f554d0c..752f72538c 100644 --- a/packages/i18n/locales/es/app.json +++ b/packages/i18n/locales/es/app.json @@ -6687,7 +6687,9 @@ "installationFailed": "", "acceptRoutesHint": "", "shortLivedEnabledHint": "", - "shortLivedTtlMsHint": "" + "shortLivedTtlMsHint": "", + "cloudflareTunnelURL": "", + "tunnelURL": "" }, "researchGlobal": { "advancedExternalSearchProviders": "", diff --git a/packages/i18n/locales/fr/app.json b/packages/i18n/locales/fr/app.json index 2def887be2..512665d580 100644 --- a/packages/i18n/locales/fr/app.json +++ b/packages/i18n/locales/fr/app.json @@ -6687,7 +6687,9 @@ "installationFailed": "", "acceptRoutesHint": "", "shortLivedEnabledHint": "", - "shortLivedTtlMsHint": "" + "shortLivedTtlMsHint": "", + "cloudflareTunnelURL": "", + "tunnelURL": "" }, "researchGlobal": { "advancedExternalSearchProviders": "", diff --git a/packages/i18n/locales/ko/app.json b/packages/i18n/locales/ko/app.json index 80ed4dd535..9d27a1f674 100644 --- a/packages/i18n/locales/ko/app.json +++ b/packages/i18n/locales/ko/app.json @@ -6687,7 +6687,9 @@ "installationFailed": "", "acceptRoutesHint": "", "shortLivedEnabledHint": "", - "shortLivedTtlMsHint": "" + "shortLivedTtlMsHint": "", + "cloudflareTunnelURL": "", + "tunnelURL": "" }, "researchGlobal": { "advancedExternalSearchProviders": "", diff --git a/packages/i18n/locales/pt-BR/app.json b/packages/i18n/locales/pt-BR/app.json index ad030784a8..75c5ab6a25 100644 --- a/packages/i18n/locales/pt-BR/app.json +++ b/packages/i18n/locales/pt-BR/app.json @@ -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 ", diff --git a/packages/i18n/locales/zh-CN/app.json b/packages/i18n/locales/zh-CN/app.json index 5473d263fd..de00ef3f03 100644 --- a/packages/i18n/locales/zh-CN/app.json +++ b/packages/i18n/locales/zh-CN/app.json @@ -6687,7 +6687,9 @@ "installationFailed": "", "acceptRoutesHint": "", "shortLivedEnabledHint": "", - "shortLivedTtlMsHint": "" + "shortLivedTtlMsHint": "", + "cloudflareTunnelURL": "", + "tunnelURL": "" }, "researchGlobal": { "advancedExternalSearchProviders": "", diff --git a/packages/i18n/locales/zh-TW/app.json b/packages/i18n/locales/zh-TW/app.json index ff53152f22..8194412aa1 100644 --- a/packages/i18n/locales/zh-TW/app.json +++ b/packages/i18n/locales/zh-TW/app.json @@ -6687,7 +6687,9 @@ "installationFailed": "", "acceptRoutesHint": "", "shortLivedEnabledHint": "", - "shortLivedTtlMsHint": "" + "shortLivedTtlMsHint": "", + "cloudflareTunnelURL": "", + "tunnelURL": "" }, "researchGlobal": { "advancedExternalSearchProviders": "", diff --git a/packages/i18n/src/resources.d.ts b/packages/i18n/src/resources.d.ts index b47dc707a6..12cbf3d73b 100644 --- a/packages/i18n/src/resources.d.ts +++ b/packages/i18n/src/resources.d.ts @@ -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}}",