Files
paseo/packages/app/src/screens/new-workspace-picker-state.ts
T
Mohamed BoudraandGitHub 2cb1b041dd Start new workspaces from pasted pull requests (#2290)
* fix(workspaces): start pasted pull requests from their branch

Recognized pull request links now select their branch directly instead of
waiting for a separate confirmation. Ensure single-branch clones also record
the fetched branch so the new worktree can track it.

* fix(workspaces): keep pasted PR checkout target-safe

* fix(workspaces): make pasted PR resolution deterministic

* fix(workspaces): preserve explicit checkout selection

A recognized pull request selects itself only when its attachment is added. Once the user chooses a branch, later composer edits must not derive checkout state from existing attachments.

* fix(composer): ignore stale pull request lookups

A lookup may finish after the workspace target changes. Only apply its attachment and selection event when the host, working directory, client, and remote still match the target that started it.

* fix(workspaces): make checkout choices authoritative

Arm automatic pull request selection only when a new PR link is detected. An explicit picker choice cancels that pending selection, existing draft attachments never infer checkout state, and target changes suppress carried-over lookups.

* fix(workspaces): stabilize pull request auto-selection

Keep the first PR selected when one edit contains several links, and accept a completed lookup when only the transport client changed for the same host and checkout.

* fix(composer): release removed pull request lookups

Release each auto-attach invocation when its composer state is abandoned, while continuing to discard any stale response.
2026-07-21 15:20:05 +02:00

98 lines
3.0 KiB
TypeScript

import {
NEW_WORKSPACE_PICKER_ATTACHMENT_OWNER,
type UserComposerAttachment,
} from "@/attachments/types";
import type { PickerItem } from "./new-workspace-picker-item";
export interface PickerSelectionState {
selectedItem: PickerItem | null;
allowAutoPrSelection: boolean;
}
export type PickerSelectionEvent =
| { type: "pr-detected" }
| { type: "pr-added"; item: Extract<PickerItem, { kind: "github-pr" }> }
| { type: "picker-selected"; item: PickerItem }
| { type: "target-changed" };
export const initialPickerSelectionState: PickerSelectionState = {
selectedItem: null,
allowAutoPrSelection: false,
};
export function reducePickerSelection(
state: PickerSelectionState,
event: PickerSelectionEvent,
): PickerSelectionState {
switch (event.type) {
case "pr-detected":
return { ...state, allowAutoPrSelection: true };
case "pr-added":
return state.allowAutoPrSelection
? { selectedItem: event.item, allowAutoPrSelection: false }
: state;
case "picker-selected":
return { selectedItem: event.item, allowAutoPrSelection: false };
case "target-changed":
return initialPickerSelectionState;
}
}
function isPrAttachment(
attachment: UserComposerAttachment,
): attachment is Extract<UserComposerAttachment, { kind: "forge_change_request" | "github_pr" }> {
return attachment.kind === "forge_change_request" || attachment.kind === "github_pr";
}
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[];
item: PickerItem | null;
}): UserComposerAttachment[] {
const nextAttachments = input.attachments.filter(
(attachment) => !isPickerOwnedPrAttachment(attachment),
);
if (input.item?.kind === "github-pr") {
const selectedPr = input.item.item;
const hasExistingPrAttachment = nextAttachments.some(
(attachment) => isPrAttachment(attachment) && attachment.item.number === selectedPr.number,
);
if (!hasExistingPrAttachment) {
return [
...nextAttachments,
{
kind: "github_pr",
item: selectedPr,
owner: NEW_WORKSPACE_PICKER_ATTACHMENT_OWNER,
},
];
}
}
return nextAttachments;
}
export function clearPickerPrAttachmentForTargetChange(input: {
attachments: UserComposerAttachment[];
currentTargetId: string;
nextTargetId: string;
}): UserComposerAttachment[] {
if (input.currentTargetId === input.nextTargetId) {
return input.attachments;
}
return input.attachments.filter((attachment) => !isPrAttachment(attachment));
}