From 0900a3863085ca341d9a9f60c53e8b45b0fbbfc7 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 3 Jul 2026 15:58:56 -0700 Subject: [PATCH] FN-7478: add local server desktop switch option Restore the desktop connection manager's local server destination alongside saved remotes. - Add a Local Server card for desktop-shell switching with active state handling. - Switch remote profile selection into remote mode before activating the profile. - Cover desktop local/remote switching behavior with tests and localized labels. - Document the switch-server local option and add the published package changeset. Files changed: .changeset/fn-7478-local-server-switch-option.md | 7 +++ docs/native-shell.md | 3 +- .../components/NativeShellConnectionManager.css | 19 ++++++ .../components/NativeShellConnectionManager.tsx | 55 ++++++++++++++--- .../NativeShellConnectionManager.test.tsx | 71 ++++++++++++++++++++-- packages/i18n/locales/en/app.json | 5 +- packages/i18n/locales/es/app.json | 5 +- packages/i18n/locales/fr/app.json | 5 +- packages/i18n/locales/ko/app.json | 5 +- packages/i18n/locales/zh-CN/app.json | 5 +- packages/i18n/locales/zh-TW/app.json | 5 +- packages/i18n/src/resources.d.ts | 3 + 12 files changed, 168 insertions(+), 20 deletions(-) Fusion-Task-Id: FN-7478 Fusion-Task-Lineage: 86a94faa-4799-46cc-aaed-b7901ee9698c Co-authored-by: Fusion (runfusion.ai) --- .../fn-7478-local-server-switch-option.md | 7 ++ docs/native-shell.md | 3 +- .../NativeShellConnectionManager.css | 19 +++++ .../NativeShellConnectionManager.tsx | 55 +++++++++++--- .../NativeShellConnectionManager.test.tsx | 71 +++++++++++++++++-- packages/i18n/locales/en/app.json | 5 +- packages/i18n/locales/es/app.json | 5 +- packages/i18n/locales/fr/app.json | 5 +- packages/i18n/locales/ko/app.json | 5 +- packages/i18n/locales/zh-CN/app.json | 5 +- packages/i18n/locales/zh-TW/app.json | 5 +- packages/i18n/src/resources.d.ts | 3 + 12 files changed, 168 insertions(+), 20 deletions(-) create mode 100644 .changeset/fn-7478-local-server-switch-option.md diff --git a/.changeset/fn-7478-local-server-switch-option.md b/.changeset/fn-7478-local-server-switch-option.md new file mode 100644 index 0000000000..26378a00a4 --- /dev/null +++ b/.changeset/fn-7478-local-server-switch-option.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Restore Local Server in the desktop Switch server list. +category: fix +dev: Desktop Connection Manager now lists local and saved remote destinations together. diff --git a/docs/native-shell.md b/docs/native-shell.md index 38e40c14ce..be03bbad3d 100644 --- a/docs/native-shell.md +++ b/docs/native-shell.md @@ -57,7 +57,8 @@ Profiles are first-class saved objects shared by onboarding and Connection Manag Connection Manager supports: -- **Use** (activate profile) +- **Desktop Switch server** shows **Local Server** as a selectable destination in the same list as saved remote/server profiles. Selecting **Local Server** calls `setDesktopMode("local")` and returns the shell to the embedded/local Fusion server without deleting remote profiles. +- **Use** (activate a saved remote profile). In desktop local mode, using a remote profile first switches desktop mode back to `remote`, then activates the selected profile. - **Edit** (update name/URL/token) - **Delete** - **Add connection** diff --git a/packages/dashboard/app/components/NativeShellConnectionManager.css b/packages/dashboard/app/components/NativeShellConnectionManager.css index 4a755da08c..345c428e06 100644 --- a/packages/dashboard/app/components/NativeShellConnectionManager.css +++ b/packages/dashboard/app/components/NativeShellConnectionManager.css @@ -24,6 +24,25 @@ gap: var(--space-md); } +.native-shell-connection-manager__destination { + width: 100%; + border-color: var(--border); + color: var(--text); + text-align: start; + cursor: pointer; +} + +.native-shell-connection-manager__destination:hover, +.native-shell-connection-manager__destination:focus-visible { + border-color: var(--accent-primary); +} + +.native-shell-connection-manager__destination > span:first-child { + display: flex; + flex-direction: column; + gap: var(--space-xs); +} + .native-shell-connection-manager__profile-actions { display: inline-flex; gap: var(--space-sm); diff --git a/packages/dashboard/app/components/NativeShellConnectionManager.tsx b/packages/dashboard/app/components/NativeShellConnectionManager.tsx index a9ca936c39..372e32044b 100644 --- a/packages/dashboard/app/components/NativeShellConnectionManager.tsx +++ b/packages/dashboard/app/components/NativeShellConnectionManager.tsx @@ -82,6 +82,32 @@ export function NativeShellConnectionManager({ open, shellApi, shellState, onClo resetEditor(); }; + const isDesktopShell = shellState.host === "desktop-shell"; + const isDesktopLocalActive = isDesktopShell && shellState.desktopMode === "local"; + const isProfileActive = (profile: ShellConnectionProfile) => ( + shellState.activeProfileId === profile.id && (!isDesktopShell || shellState.desktopMode === "remote") + ); + + const handleUseLocalServer = async () => { + resetEditor(); + setDeleteCandidate(null); + await shellApi.setDesktopMode("local"); + }; + + const handleUseProfile = async (profileId: string) => { + resetEditor(); + if (isDesktopShell) { + /* + * FNXC:DesktopSwitchServer 2026-07-03-00:00: + * Desktop Switch server presents local and remote destinations in one list. A remote profile + * selection must explicitly enter remote mode before activation, while the Local Server entry + * is the symmetric return path to the embedded runtime without deleting saved profiles. + */ + await shellApi.setDesktopMode("remote"); + } + await shellApi.setActiveProfile(profileId); + }; + return (
@@ -92,14 +118,25 @@ export function NativeShellConnectionManager({ open, shellApi, shellState, onClo
- {shellState.host === "desktop-shell" && ( -
- - -
- )} -
+ {isDesktopShell && ( + + )} + {shellState.profiles.length === 0 ? (

{t("shell.noServersSaved", "No remote servers saved yet.")}

@@ -128,7 +165,7 @@ export function NativeShellConnectionManager({ open, shellApi, shellState, onClo
{profile.name}
{profile.serverUrl}
- {profile.id === shellState.activeProfileId && {t("shell.activePill", "Active")}} + {isProfileActive(profile) && {t("shell.activePill", "Active")}}
- +
diff --git a/packages/dashboard/app/components/__tests__/NativeShellConnectionManager.test.tsx b/packages/dashboard/app/components/__tests__/NativeShellConnectionManager.test.tsx index 6f1f4b0bfe..f95d0b7056 100644 --- a/packages/dashboard/app/components/__tests__/NativeShellConnectionManager.test.tsx +++ b/packages/dashboard/app/components/__tests__/NativeShellConnectionManager.test.tsx @@ -22,20 +22,81 @@ function createShellApi() { }; } +const remoteProfile = { + id: "remote-1", + name: "Remote", + serverUrl: "https://fusion.example.com", + authToken: "token-value", + createdAt: "", + updatedAt: "", +}; + describe("NativeShellConnectionManager", () => { - it("switches desktop mode", async () => { + it("shows a desktop Local Server destination and switches back from an active remote profile", async () => { const shellApi = createShellApi(); render( , ); - fireEvent.click(screen.getByText("Local")); + expect(screen.getByRole("button", { name: /Local Server/i })).toBeInTheDocument(); + expect(screen.getByText("Remote")).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: /Local Server/i })); + await waitFor(() => expect(shellApi.setDesktopMode).toHaveBeenCalledWith("local")); + expect(shellApi.setActiveProfile).not.toHaveBeenCalled(); + }); + + it("marks Local Server active in desktop local mode and switches remote profile use into remote mode", async () => { + const shellApi = createShellApi(); + render( + , + ); + + expect(screen.getByRole("button", { name: /Local Server/i })).toHaveAttribute("aria-pressed", "true"); + expect(screen.getByText("Active")).toBeInTheDocument(); + + fireEvent.click(screen.getAllByLabelText("Use Remote")[1]!); + + await waitFor(() => { + expect(shellApi.setDesktopMode).toHaveBeenCalledWith("remote"); + expect(shellApi.setActiveProfile).toHaveBeenCalledWith("remote-2"); + }); + expect(shellApi.setDesktopMode.mock.invocationCallOrder[0]).toBeLessThan(shellApi.setActiveProfile.mock.invocationCallOrder[0]); + }); + + it("renders Local Server deterministically with no remote profiles and an unset desktop mode", () => { + const shellApi = createShellApi(); + render( + , + ); + + expect(screen.getByRole("button", { name: /Local Server/i })).toBeInTheDocument(); + expect(screen.getByText("No remote servers saved yet.")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Add server" })).toBeInTheDocument(); }); it("shows active profile indicator and requires delete confirmation", async () => { @@ -49,6 +110,7 @@ describe("NativeShellConnectionManager", () => { />, ); + expect(screen.queryByRole("button", { name: /Local Server/i })).toBeNull(); expect(screen.getByText("Active")).toBeInTheDocument(); fireEvent.click(screen.getByLabelText("Delete Prod")); expect(screen.getByRole("alertdialog", { name: "Delete server confirmation" })).toBeInTheDocument(); @@ -79,7 +141,7 @@ describe("NativeShellConnectionManager", () => { }); }); - it("supports empty-state recovery and QR import", async () => { + it("supports empty-state recovery and QR import without a mobile Local Server option", async () => { const shellApi = createShellApi(); render( { />, ); + expect(screen.queryByRole("button", { name: /Local Server/i })).toBeNull(); expect(screen.getByText("No remote servers saved yet.")).toBeInTheDocument(); fireEvent.click(screen.getAllByText("Scan QR")[0]!); diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json index b810bd31a7..ac071cb0dd 100644 --- a/packages/i18n/locales/en/app.json +++ b/packages/i18n/locales/en/app.json @@ -6947,7 +6947,10 @@ "serverUrlLabel": "Server URL", "serverUrlProtocolError": "Server URL must use http or https", "use": "Use", - "useProfile": "Use {{name}}" + "useProfile": "Use {{name}}", + "localServerTitle": "Local Server", + "localServerDescription": "Use the embedded Fusion server on this device.", + "localServerActive": "Current" }, "skills": { "addSkill": "Add a skill…", diff --git a/packages/i18n/locales/es/app.json b/packages/i18n/locales/es/app.json index 48b29297bf..030ddad2a4 100644 --- a/packages/i18n/locales/es/app.json +++ b/packages/i18n/locales/es/app.json @@ -6937,7 +6937,10 @@ "serverUrlLabel": "URL del servidor", "serverUrlProtocolError": "La URL del servidor debe usar http o https", "use": "Usar", - "useProfile": "Usar {{name}}" + "useProfile": "Usar {{name}}", + "localServerTitle": "Servidor local", + "localServerDescription": "Usa el servidor Fusion integrado en este dispositivo.", + "localServerActive": "Actual" }, "skills": { "addSkill": "Añadir una habilidad…", diff --git a/packages/i18n/locales/fr/app.json b/packages/i18n/locales/fr/app.json index 0a1410ffc9..6f35abc2fc 100644 --- a/packages/i18n/locales/fr/app.json +++ b/packages/i18n/locales/fr/app.json @@ -6937,7 +6937,10 @@ "serverUrlLabel": "URL du serveur", "serverUrlProtocolError": "L'URL du serveur doit utiliser http ou https", "use": "Utiliser", - "useProfile": "Utiliser {{name}}" + "useProfile": "Utiliser {{name}}", + "localServerTitle": "Serveur local", + "localServerDescription": "Utiliser le serveur Fusion intégré sur cet appareil.", + "localServerActive": "Actuel" }, "skills": { "addSkill": "Ajouter une compétence…", diff --git a/packages/i18n/locales/ko/app.json b/packages/i18n/locales/ko/app.json index 2e07f1e3df..9f0415402d 100644 --- a/packages/i18n/locales/ko/app.json +++ b/packages/i18n/locales/ko/app.json @@ -6937,7 +6937,10 @@ "serverUrlLabel": "서버 URL", "serverUrlProtocolError": "서버 URL은 http 또는 https를 사용해야 합니다", "use": "사용", - "useProfile": "{{name}} 사용" + "useProfile": "{{name}} 사용", + "localServerTitle": "로컬 서버", + "localServerDescription": "이 기기의 내장 Fusion 서버를 사용합니다.", + "localServerActive": "현재" }, "skills": { "addSkill": "스킬 추가…", diff --git a/packages/i18n/locales/zh-CN/app.json b/packages/i18n/locales/zh-CN/app.json index d3fd442234..b7bd443c3c 100644 --- a/packages/i18n/locales/zh-CN/app.json +++ b/packages/i18n/locales/zh-CN/app.json @@ -6937,7 +6937,10 @@ "serverUrlLabel": "服务器 URL", "serverUrlProtocolError": "服务器 URL 必须使用 http 或 https", "use": "使用", - "useProfile": "使用 {{name}}" + "useProfile": "使用 {{name}}", + "localServerTitle": "本地服务器", + "localServerDescription": "使用此设备上的内置 Fusion 服务器。", + "localServerActive": "当前" }, "skills": { "addSkill": "添加技能…", diff --git a/packages/i18n/locales/zh-TW/app.json b/packages/i18n/locales/zh-TW/app.json index b2052bbcbd..619e755202 100644 --- a/packages/i18n/locales/zh-TW/app.json +++ b/packages/i18n/locales/zh-TW/app.json @@ -6937,7 +6937,10 @@ "serverUrlLabel": "伺服器 URL", "serverUrlProtocolError": "伺服器 URL 必須使用 http 或 https", "use": "使用", - "useProfile": "使用 {{name}}" + "useProfile": "使用 {{name}}", + "localServerTitle": "本機伺服器", + "localServerDescription": "使用此裝置上的內建 Fusion 伺服器。", + "localServerActive": "目前" }, "skills": { "addSkill": "新增技能…", diff --git a/packages/i18n/src/resources.d.ts b/packages/i18n/src/resources.d.ts index 289bf4b173..e5284a1fd6 100644 --- a/packages/i18n/src/resources.d.ts +++ b/packages/i18n/src/resources.d.ts @@ -6901,6 +6901,9 @@ export default interface Resources { "deleteConfirmMessage": "Delete {{name}}? This removes the saved profile.", "deleteProfile": "Delete {{name}}", "editProfile": "Edit {{name}}", + "localServerActive": "Current", + "localServerDescription": "Use the embedded Fusion server on this device.", + "localServerTitle": "Local Server", "modeLocal": "Local", "modeRemote": "Remote", "nameLabel": "Name",