From baaaaa363df6c703663e2c0f6b99d91b6e82c499 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 2 Nov 2026 09:53:39 +0100 Subject: [PATCH] fix(plugins): a host switch drops the branch and metadata even when the project id repeats A remote repository keeps the same project id on every host, so comparing the project id alone missed a switch from host A to host B for the same repository. The target is now the host and the project id together. --- .../src/plugins/new-workspace-items/model.test.ts | 1 + packages/app/src/screens/new-workspace-screen.tsx | 2 +- .../src/screens/new-workspace/setup-model.test.ts | 12 ++++++++++++ .../app/src/screens/new-workspace/setup-model.ts | 15 +++++++++------ 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/app/src/plugins/new-workspace-items/model.test.ts b/packages/app/src/plugins/new-workspace-items/model.test.ts index 827c54280d..4806b9dcd4 100644 --- a/packages/app/src/plugins/new-workspace-items/model.test.ts +++ b/packages/app/src/plugins/new-workspace-items/model.test.ts @@ -39,6 +39,7 @@ describe("the base ref a plugin reads", () => { const chosen: PickerItem[] = []; const setup = createNewWorkspaceSetup( { + serverId: "host-a", locked: false, project: { id: "p", name: "repo" }, canCreateWorktree: true, diff --git a/packages/app/src/screens/new-workspace-screen.tsx b/packages/app/src/screens/new-workspace-screen.tsx index 67a9d33511..cfb8fd8b9c 100644 --- a/packages/app/src/screens/new-workspace-screen.tsx +++ b/packages/app/src/screens/new-workspace-screen.tsx @@ -1654,7 +1654,7 @@ function usePluginSetup(input: { () => (projectId && projectName ? { id: projectId, name: projectName } : null), [projectId, projectName], ); - const inputs = { locked, project: pluginProject, canCreateWorktree, supportsMetadata }; + const inputs = { serverId, locked, project: pluginProject, canCreateWorktree, supportsMetadata }; // Both are stable for the life of the screen: preferences write through the query client, and // the draft is keyed per mount. A command and a pick run the same effects. const [setup] = useState(() => diff --git a/packages/app/src/screens/new-workspace/setup-model.test.ts b/packages/app/src/screens/new-workspace/setup-model.test.ts index bdbc4e5f46..2f2ee2612d 100644 --- a/packages/app/src/screens/new-workspace/setup-model.test.ts +++ b/packages/app/src/screens/new-workspace/setup-model.test.ts @@ -3,6 +3,7 @@ import type { PickerItem } from "../new-workspace-picker-item"; import { createNewWorkspaceSetup, type SetupEffects, type SetupInputs } from "./setup-model"; const READY: SetupInputs = { + serverId: "host-a", locked: false, project: { id: "p1", name: "repo" }, canCreateWorktree: true, @@ -76,6 +77,17 @@ describe("New workspace setup commands", () => { expect(setup.getSnapshot().metadata).toEqual({}); }); + it("drops the branch and metadata when the host changes under the same project id", async () => { + const setup = createNewWorkspaceSetup(READY, recordingEffects().effects); + await setup.commands.setBranch("pas-101"); + await setup.commands.setMetadata({ issue: "PAS-101" }); + + setup.syncInputs({ ...READY, serverId: "host-b" }); + + expect(setup.getSnapshot().branch).toBeNull(); + expect(setup.getSnapshot().metadata).toEqual({}); + }); + it("rejects every command while the screen is locked, as the pickers do", async () => { const setup = createNewWorkspaceSetup({ ...READY, locked: true }, recordingEffects().effects); diff --git a/packages/app/src/screens/new-workspace/setup-model.ts b/packages/app/src/screens/new-workspace/setup-model.ts index ce0098becb..acf90ab2f7 100644 --- a/packages/app/src/screens/new-workspace/setup-model.ts +++ b/packages/app/src/screens/new-workspace/setup-model.ts @@ -26,6 +26,8 @@ interface SetupState { * commit, so a command reads it at the moment it runs rather than from the render that built it. */ export interface SetupInputs { + /** The host the screen creates on; a project id alone can repeat across hosts. */ + serverId: string; locked: boolean; project: { id: string; name: string } | null; canCreateWorktree: boolean; @@ -80,7 +82,7 @@ export function createNewWorkspaceSetup( metadata: {}, }; let inputs = initial; - let target = initial.project?.id ?? null; + let target = initial.project ? `${initial.serverId}\n${initial.project.id}` : null; const listeners = new Set<() => void>(); const update = (patch: Partial) => { state = { ...state, ...patch }; @@ -111,12 +113,13 @@ export function createNewWorkspaceSetup( chooseIsolation, syncInputs: (next) => { inputs = next; - // A branch and metadata are chosen for one project on one host. A project id is - // host-scoped, so a change of host changes it too. An absent project is a screen that is - // still resolving, not a switch. + // A branch and metadata are chosen for one project on one host. A project id can repeat + // across hosts (a remote repository keeps its key everywhere), so the host is part of the + // target. An absent project is a screen that is still resolving, not a switch. if (!next.project) return; - if (target && target !== next.project.id) update({ branch: null, metadata: {} }); - target = next.project.id; + const nextTarget = `${next.serverId}\n${next.project.id}`; + if (target && target !== nextTarget) update({ branch: null, metadata: {} }); + target = nextTarget; }, commands: { async setIsolation(isolation) {