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.
This commit is contained in:
Mohamed Boudra
2026-11-02 09:53:39 +01:00
parent 16b843949e
commit baaaaa363d
4 changed files with 23 additions and 7 deletions
@@ -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,
@@ -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(() =>
@@ -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);
@@ -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<SetupState>) => {
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) {