Keep New Workspace prompts when switching projects or hosts (#2036)

* fix(app): preserve New Workspace prompts across target changes

The New Workspace draft was keyed by the selected host and project, so changing either target replaced the visible prompt with another draft scope. Treat the screen as one persistent draft surface and migrate the newest active legacy draft.

* test(app): express draft persistence as user actions

* fix(app): clear stale picker PR context

* fix(app): persist picker attachment ownership

* fix(app): keep workspace target context safe
This commit is contained in:
Mohamed Boudra
2026-07-13 15:36:48 +02:00
committed by GitHub
parent 218097b7cc
commit 13d6ad598e
11 changed files with 462 additions and 79 deletions
@@ -1,37 +1,60 @@
import type { UserComposerAttachment } from "@/attachments/types";
import {
NEW_WORKSPACE_PICKER_ATTACHMENT_OWNER,
type UserComposerAttachment,
} from "@/attachments/types";
import type { PickerItem } from "./new-workspace-picker-item";
// The picker "owns" at most one PR attachment at a time. When the user selects
// a different item the previously-owned PR is removed before the new one is added.
// User-added attachments for other PRs/issues are left untouched.
function isPickerOwnedPrAttachment(attachment: UserComposerAttachment): attachment is Extract<
UserComposerAttachment,
{ kind: "github_pr" }
> & {
owner: typeof NEW_WORKSPACE_PICKER_ATTACHMENT_OWNER;
} {
return (
attachment.kind === "github_pr" && attachment.owner === NEW_WORKSPACE_PICKER_ATTACHMENT_OWNER
);
}
// Ownership lives on the attachment because drafts outlive this component.
// The picker owns at most one PR; user-added PRs and issues remain untouched.
export function syncPickerPrAttachment(input: {
attachments: UserComposerAttachment[];
previousPickerPrNumber: number | null;
item: PickerItem;
}): { attachments: UserComposerAttachment[]; attachedPrNumber: number | null } {
let nextAttachments = input.attachments;
let attachedPrNumber: number | null = null;
item: PickerItem | null;
}): UserComposerAttachment[] {
const nextAttachments = input.attachments.filter(
(attachment) => !isPickerOwnedPrAttachment(attachment),
);
if (input.previousPickerPrNumber !== null) {
nextAttachments = nextAttachments.filter(
(attachment) =>
attachment.kind !== "github_pr" || attachment.item.number !== input.previousPickerPrNumber,
);
}
if (input.item.kind === "github-pr") {
if (input.item?.kind === "github-pr") {
const selectedPr = input.item.item;
const hasExistingPrAttachment = nextAttachments.some(
(attachment) =>
attachment.kind === "github_pr" && attachment.item.number === selectedPr.number,
);
if (!hasExistingPrAttachment) {
nextAttachments = [...nextAttachments, { kind: "github_pr", item: selectedPr }];
attachedPrNumber = selectedPr.number;
return [
...nextAttachments,
{
kind: "github_pr",
item: selectedPr,
owner: NEW_WORKSPACE_PICKER_ATTACHMENT_OWNER,
},
];
}
}
return { attachments: nextAttachments, attachedPrNumber };
return nextAttachments;
}
export function clearPickerPrAttachmentForTargetChange(input: {
attachments: UserComposerAttachment[];
currentTargetId: string;
nextTargetId: string;
}): UserComposerAttachment[] {
if (input.currentTargetId === input.nextTargetId) {
return input.attachments;
}
return syncPickerPrAttachment({ attachments: input.attachments, item: null });
}
export function findCheckoutHintPrAttachment(input: {