From 619daa148941c6c23503d6a352779f6b6f344fc4 Mon Sep 17 00:00:00 2001 From: Vadim Lazuk Date: Sat, 7 Nov 2026 15:18:04 +0300 Subject: [PATCH] fix(remote): keep auxiliary windows' remote routing when they hold no project (#1975) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A comparison window (Git Log > Compare with Local) of a remote project rides its parent's session: remote_current answers with the parent's connection, so its commands go to the host where the project is open. But the window never opens a project of its own — it keeps only the settings (restoring the project would overwrite the main window's editor session) — and the useProject subscription read every settings update as 'the window left the remote project', dropping the routing. The next commands then ran on the desktop, where no project is open for the window: Git Log > Compare with Local on a WSL project opened an empty comparison window with 'IpcError: no project is open in this window' (same for SSH remote projects). The subscription now only ends the session when the window's project actually leaves the session's root: it had the session's project open and now holds none or another one. Opening a local project, LightEdit and Close Project still end the routing (open_project / light_edit_open / close_project route locally and end it themselves; closeProject keeps the subscription as its safety net). --- src/platform/remote.test.ts | 50 ++++++++++++++++++++++++++++++++++++- src/platform/remote.ts | 12 +++++++-- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/platform/remote.test.ts b/src/platform/remote.test.ts index 137e3a713..74596d647 100644 --- a/src/platform/remote.test.ts +++ b/src/platform/remote.test.ts @@ -1,5 +1,8 @@ import { Channel } from "@tauri-apps/api/core"; -import { beforeAll, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, describe, expect, it } from "vitest"; +import type { ProjectInfo, Settings } from "./ipc"; +import { useProject } from "./project"; +import { disableRemote, enableRemote, useRemote } from "./remote"; import { encodeArgs, remoteRouting } from "./remote"; describe("remote invoke", () => { @@ -42,3 +45,48 @@ describe("remote invoke", () => { expect(encodeArgs(undefined).json).toBe("{}"); }); }); + +describe("remote session lifetime", () => { + const session = { + target: { kind: "wsl" as const, distro: "Ubuntu" }, + root: "/home/me/app", + host: "Ubuntu", + os: "linux", + commands: ["vcs_roots"], + }; + const project = { + name: "app", + root: "/home/me/app", + trusted: true, + isGit: true, + settings: {}, + } as unknown as ProjectInfo; + + afterEach(() => { + disableRemote(); + useProject.setState({ project: null, settings: null, opening: false, openingPath: null, lightEdit: false }); + }); + + it("keeps routing when a window with no project of its own updates the settings only", () => { + // The comparison window: it rides its parent's session and keeps just the settings + // (restoring the project would overwrite the main window's editor session). + enableRemote(session); + useProject.setState({ settings: { editor: { fontSize: 12 } } as unknown as Settings }); + expect(useRemote.getState().session).not.toBeNull(); + }); + + it("ends routing when the window's project leaves the session's root", () => { + enableRemote(session); + useProject.setState({ project }); + // Close Project (or a local project opened over the remote one) — the session goes. + useProject.setState({ project: null }); + expect(useRemote.getState().session).toBeNull(); + }); + + it("keeps routing while the project still sits at the session's root", () => { + enableRemote(session); + useProject.setState({ project }); + useProject.setState({ settings: { editor: { fontSize: 14 } } as unknown as Settings }); + expect(useRemote.getState().session).not.toBeNull(); + }); +}); diff --git a/src/platform/remote.ts b/src/platform/remote.ts index 504fb1211..fb7ef224b 100644 --- a/src/platform/remote.ts +++ b/src/platform/remote.ts @@ -338,7 +338,15 @@ export function forgetRemote(target: RemoteTarget, path: string) { } // Any way of leaving the remote project (Close Project, opening a local one) ends remote routing. -useProject.subscribe((s) => { +useProject.subscribe((s, prev) => { const session = useRemote.getState().session; - if (session && !s.opening && s.project?.root !== session.root) disableRemote(); + if (!session || s.opening) return; + // Only a window whose project was the session's root can leave it. Auxiliary windows + // (comparison, deployment) hold no project of their own — they ride their parent's + // session, and a settings-only update (the comparison window keeps just the settings) + // must not drop the routing: its next commands would run here instead of on the host, + // where no project is open (`open_project` / `light_edit_open` / `close_project` end + // the session themselves, see `route`). + if (prev.project?.root !== session.root || s.project?.root === session.root) return; + disableRemote(); });