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.
This commit is contained in:
@@ -2,7 +2,8 @@ import { describe, expect, it } from "vitest";
|
||||
import type { UserComposerAttachment } from "@/attachments/types";
|
||||
import {
|
||||
clearPickerPrAttachmentForTargetChange,
|
||||
findCheckoutHintPrAttachment,
|
||||
initialPickerSelectionState,
|
||||
reducePickerSelection,
|
||||
syncPickerPrAttachment,
|
||||
} from "./new-workspace-picker-state";
|
||||
import type { ForgeSearchItem } from "@getpaseo/protocol/messages";
|
||||
@@ -28,21 +29,28 @@ function prAttachment(
|
||||
return { kind: "github_pr", item, ...(owner ? { owner } : {}) };
|
||||
}
|
||||
|
||||
function issueAttachment(number: number): UserComposerAttachment {
|
||||
function forgePrAttachment(
|
||||
item: ForgeSearchItem,
|
||||
): Extract<UserComposerAttachment, { kind: "forge_change_request" }> {
|
||||
return { kind: "forge_change_request", item };
|
||||
}
|
||||
|
||||
function makeIssueItem(number: number): ForgeSearchItem {
|
||||
return {
|
||||
kind: "github_issue",
|
||||
item: {
|
||||
kind: "issue",
|
||||
number,
|
||||
title: `Issue ${number}`,
|
||||
url: `https://example.com/issues/${number}`,
|
||||
state: "open",
|
||||
body: null,
|
||||
labels: [],
|
||||
},
|
||||
kind: "issue",
|
||||
number,
|
||||
title: `Issue ${number}`,
|
||||
url: `https://example.com/issues/${number}`,
|
||||
state: "open",
|
||||
body: null,
|
||||
labels: [],
|
||||
};
|
||||
}
|
||||
|
||||
function issueAttachment(number: number): UserComposerAttachment {
|
||||
return { kind: "github_issue", item: makeIssueItem(number) };
|
||||
}
|
||||
|
||||
describe("syncPickerPrAttachment", () => {
|
||||
it("selects a PR when no previous picker PR is set", () => {
|
||||
const pr = makePrItem(202, "Refactor picker");
|
||||
@@ -91,6 +99,15 @@ describe("syncPickerPrAttachment", () => {
|
||||
expect(result).toEqual([prAttachment(pr)]);
|
||||
});
|
||||
|
||||
it("does not duplicate a generalized PR attachment", () => {
|
||||
const pr = makePrItem(202, "Refactor picker");
|
||||
const result = syncPickerPrAttachment({
|
||||
attachments: [forgePrAttachment(pr)],
|
||||
item: { kind: "github-pr", item: pr },
|
||||
});
|
||||
expect(result).toEqual([forgePrAttachment(pr)]);
|
||||
});
|
||||
|
||||
it("clears a persisted picker selection without removing user-added attachments", () => {
|
||||
const pickerPr = prAttachment(makePrItem(202, "Picker PR"), "new-workspace-picker");
|
||||
const manuallyAttachedPr = prAttachment(makePrItem(303, "Manual PR"));
|
||||
@@ -119,67 +136,82 @@ describe("clearPickerPrAttachmentForTargetChange", () => {
|
||||
).toBe(attachments);
|
||||
});
|
||||
|
||||
it("clears only the picker-owned PR when the target changes", () => {
|
||||
it("clears all PR attachments when the target changes", () => {
|
||||
const pickerPr = prAttachment(makePrItem(202, "Picker PR"), "new-workspace-picker");
|
||||
const manualPr = prAttachment(makePrItem(303, "Manual PR"));
|
||||
const forgePr = forgePrAttachment(makePrItem(404, "Forge PR"));
|
||||
const issue = issueAttachment(44);
|
||||
|
||||
expect(
|
||||
clearPickerPrAttachmentForTargetChange({
|
||||
attachments: [pickerPr, manualPr],
|
||||
attachments: [issue, pickerPr, manualPr, forgePr],
|
||||
currentTargetId: "server-a",
|
||||
nextTargetId: "server-b",
|
||||
}),
|
||||
).toEqual([manualPr]);
|
||||
).toEqual([issue]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findCheckoutHintPrAttachment", () => {
|
||||
it("returns the first attached PR that is not selected or dismissed", () => {
|
||||
const first = prAttachment(makePrItem(101, "A"));
|
||||
const second = prAttachment(makePrItem(202, "B"));
|
||||
describe("reducePickerSelection", () => {
|
||||
it("selects a PR that was newly detected and added", () => {
|
||||
const item = { kind: "github-pr" as const, item: makePrItem(101, "A") };
|
||||
const detected = reducePickerSelection(initialPickerSelectionState, { type: "pr-detected" });
|
||||
|
||||
expect(
|
||||
findCheckoutHintPrAttachment({
|
||||
attachments: [issueAttachment(44), first, second],
|
||||
selectedItem: null,
|
||||
dismissedPrNumbers: new Set(),
|
||||
}),
|
||||
).toBe(first);
|
||||
expect(reducePickerSelection(detected, { type: "pr-added", item })).toEqual({
|
||||
selectedItem: item,
|
||||
allowAutoPrSelection: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("skips the selected PR and offers the next attached PR", () => {
|
||||
const selected = prAttachment(makePrItem(101, "A"));
|
||||
const next = prAttachment(makePrItem(202, "B"));
|
||||
it("keeps the first PR selected when one edit adds multiple PRs", () => {
|
||||
const detected = reducePickerSelection(initialPickerSelectionState, { type: "pr-detected" });
|
||||
const first = reducePickerSelection(detected, {
|
||||
type: "pr-added",
|
||||
item: { kind: "github-pr", item: makePrItem(101, "A") },
|
||||
});
|
||||
|
||||
expect(
|
||||
findCheckoutHintPrAttachment({
|
||||
attachments: [selected, next],
|
||||
selectedItem: { kind: "github-pr", item: selected.item },
|
||||
dismissedPrNumbers: new Set(),
|
||||
reducePickerSelection(first, {
|
||||
type: "pr-added",
|
||||
item: { kind: "github-pr", item: makePrItem(202, "B") },
|
||||
}),
|
||||
).toBe(next);
|
||||
).toEqual(first);
|
||||
});
|
||||
|
||||
it("skips dismissed PRs and ignores issues", () => {
|
||||
const dismissed = prAttachment(makePrItem(101, "A"));
|
||||
const next = prAttachment(makePrItem(202, "B"));
|
||||
it("keeps a branch selected after a pending PR is added", () => {
|
||||
const detected = reducePickerSelection(initialPickerSelectionState, { type: "pr-detected" });
|
||||
const branchSelected = reducePickerSelection(detected, {
|
||||
type: "picker-selected",
|
||||
item: { kind: "branch", name: "main" },
|
||||
});
|
||||
|
||||
expect(
|
||||
findCheckoutHintPrAttachment({
|
||||
attachments: [issueAttachment(44), dismissed, next],
|
||||
selectedItem: null,
|
||||
dismissedPrNumbers: new Set([101]),
|
||||
reducePickerSelection(branchSelected, {
|
||||
type: "pr-added",
|
||||
item: { kind: "github-pr", item: makePrItem(101, "A") },
|
||||
}),
|
||||
).toBe(next);
|
||||
).toEqual(branchSelected);
|
||||
});
|
||||
|
||||
it("returns null when only issues qualify", () => {
|
||||
it("does not derive checkout selection from an existing attachment", () => {
|
||||
expect(
|
||||
findCheckoutHintPrAttachment({
|
||||
attachments: [issueAttachment(44)],
|
||||
selectedItem: null,
|
||||
dismissedPrNumbers: new Set(),
|
||||
reducePickerSelection(initialPickerSelectionState, {
|
||||
type: "pr-added",
|
||||
item: { kind: "github-pr", item: makePrItem(101, "A") },
|
||||
}),
|
||||
).toBeNull();
|
||||
).toEqual(initialPickerSelectionState);
|
||||
});
|
||||
|
||||
it("lets a newly detected PR replace an earlier explicit branch", () => {
|
||||
const branchSelected = reducePickerSelection(initialPickerSelectionState, {
|
||||
type: "picker-selected",
|
||||
item: { kind: "branch", name: "main" },
|
||||
});
|
||||
const detected = reducePickerSelection(branchSelected, { type: "pr-detected" });
|
||||
const pr = { kind: "github-pr" as const, item: makePrItem(101, "A") };
|
||||
|
||||
expect(reducePickerSelection(detected, { type: "pr-added", item: pr }).selectedItem).toEqual(
|
||||
pr,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,46 @@ import {
|
||||
} 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" }
|
||||
@@ -28,8 +68,7 @@ export function syncPickerPrAttachment(input: {
|
||||
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,
|
||||
(attachment) => isPrAttachment(attachment) && attachment.item.number === selectedPr.number,
|
||||
);
|
||||
if (!hasExistingPrAttachment) {
|
||||
return [
|
||||
@@ -54,24 +93,5 @@ export function clearPickerPrAttachmentForTargetChange(input: {
|
||||
if (input.currentTargetId === input.nextTargetId) {
|
||||
return input.attachments;
|
||||
}
|
||||
return syncPickerPrAttachment({ attachments: input.attachments, item: null });
|
||||
}
|
||||
|
||||
export function findCheckoutHintPrAttachment(input: {
|
||||
attachments: ReadonlyArray<UserComposerAttachment>;
|
||||
selectedItem: PickerItem | null;
|
||||
dismissedPrNumbers: ReadonlySet<number>;
|
||||
}): Extract<UserComposerAttachment, { kind: "github_pr" }> | null {
|
||||
const selectedPrNumber =
|
||||
input.selectedItem?.kind === "github-pr" ? input.selectedItem.item.number : null;
|
||||
|
||||
for (const attachment of input.attachments) {
|
||||
if (attachment.kind !== "github_pr") continue;
|
||||
const prNumber = attachment.item.number;
|
||||
if (prNumber === selectedPrNumber) continue;
|
||||
if (input.dismissedPrNumbers.has(prNumber)) continue;
|
||||
return attachment;
|
||||
}
|
||||
|
||||
return null;
|
||||
return input.attachments.filter((attachment) => !isPrAttachment(attachment));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
|
||||
import type { ReactElement, RefObject } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
@@ -9,15 +9,7 @@ import { StyleSheet, useUnistyles, withUnistyles } from "react-native-unistyles"
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { createNameId } from "mnemonic-id";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
Folder,
|
||||
FolderPlus,
|
||||
GitBranch,
|
||||
GitPullRequest,
|
||||
X,
|
||||
} from "lucide-react-native";
|
||||
import { ChevronDown, Folder, FolderPlus, GitBranch, GitPullRequest } from "lucide-react-native";
|
||||
import { Composer } from "@/composer";
|
||||
import { FileDropZone } from "@/components/file-drop/file-drop-zone";
|
||||
import { DraftAgentModeControl } from "@/composer/agent-controls/mode-control";
|
||||
@@ -80,7 +72,7 @@ import {
|
||||
} from "@/projects/host-projects";
|
||||
import { useProjectIconDataByProjectKey } from "@/projects/project-icons";
|
||||
import { ICON_SIZE, type Theme } from "@/styles/theme";
|
||||
import type { ComposerAttachment, UserComposerAttachment } from "@/attachments/types";
|
||||
import type { ComposerAttachment } from "@/attachments/types";
|
||||
import { useDraftWorkspaceAttachmentScopeKey } from "@/attachments/workspace-attachments-store";
|
||||
import type { MessagePayload } from "@/composer/types";
|
||||
import type { AgentAttachment, ForgeSearchItem } from "@getpaseo/protocol/messages";
|
||||
@@ -99,7 +91,8 @@ import {
|
||||
} from "./new-workspace-picker-item";
|
||||
import {
|
||||
clearPickerPrAttachmentForTargetChange,
|
||||
findCheckoutHintPrAttachment,
|
||||
initialPickerSelectionState,
|
||||
reducePickerSelection,
|
||||
syncPickerPrAttachment,
|
||||
} from "./new-workspace-picker-state";
|
||||
import {
|
||||
@@ -186,10 +179,6 @@ interface PickerOptionData {
|
||||
itemById: Map<string, PickerItem>;
|
||||
}
|
||||
|
||||
interface PickerSelection {
|
||||
item: PickerItem;
|
||||
}
|
||||
|
||||
const BRANCH_OPTION_PREFIX = "branch:";
|
||||
const PR_OPTION_PREFIX = "github-pr:";
|
||||
const PROJECT_ICON_FALLBACK_FONT_SIZE = 10;
|
||||
@@ -335,50 +324,6 @@ function ProjectPickerTrigger({
|
||||
);
|
||||
}
|
||||
|
||||
function CheckoutHintBadge({
|
||||
label,
|
||||
acceptLabel,
|
||||
dismissLabel,
|
||||
onAccept,
|
||||
onDismiss,
|
||||
iconColor,
|
||||
iconSize,
|
||||
}: {
|
||||
label: string;
|
||||
acceptLabel: string;
|
||||
dismissLabel: string;
|
||||
onAccept: () => void;
|
||||
onDismiss: () => void;
|
||||
iconColor: string;
|
||||
iconSize: number;
|
||||
}) {
|
||||
return (
|
||||
<View style={styles.checkoutHintBadge}>
|
||||
<Text style={styles.badgeText} numberOfLines={1}>
|
||||
{label}
|
||||
</Text>
|
||||
<Pressable
|
||||
testID="new-workspace-checkout-hint-accept"
|
||||
onPress={onAccept}
|
||||
style={styles.checkoutHintAction}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={acceptLabel}
|
||||
>
|
||||
<Check size={iconSize} color={iconColor} />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
testID="new-workspace-checkout-hint-dismiss"
|
||||
onPress={onDismiss}
|
||||
style={styles.checkoutHintAction}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={dismissLabel}
|
||||
>
|
||||
<X size={iconSize} color={iconColor} />
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function PickerOptionItem({
|
||||
testID,
|
||||
label,
|
||||
@@ -646,14 +591,6 @@ function formatPrLabel(item: Pick<ForgeSearchItem, "forge" | "number" | "title">
|
||||
return `${presentation.numberPrefix}${item.number} ${item.title}`;
|
||||
}
|
||||
|
||||
function getCheckoutHintPresentation(item: ForgeSearchItem) {
|
||||
const presentation = getForgePresentation(item.forge ?? "github");
|
||||
return {
|
||||
noun: presentation.changeRequestAbbrev,
|
||||
numberPrefix: presentation.numberPrefix,
|
||||
};
|
||||
}
|
||||
|
||||
function pickerItemLabel(item: PickerItem): string {
|
||||
return item.kind === "branch" ? item.name : formatPrLabel(item.item);
|
||||
}
|
||||
@@ -803,11 +740,6 @@ function getContentStyle(input: { isCompact: boolean; insetBottom: number }) {
|
||||
return [styles.content, styles.contentCentered];
|
||||
}
|
||||
|
||||
function getSelectedPickerItem(selection: PickerSelection | null): PickerItem | null {
|
||||
if (!selection) return null;
|
||||
return selection.item;
|
||||
}
|
||||
|
||||
function normalizeBranchDetails(
|
||||
data:
|
||||
| { branchDetails?: Array<{ name: string; committerDate: number }>; branches?: string[] }
|
||||
@@ -1060,47 +992,6 @@ function buildComposerConfig(input: {
|
||||
};
|
||||
}
|
||||
|
||||
function collectAttachedPrNumbers(attachments: ReadonlyArray<UserComposerAttachment>): Set<number> {
|
||||
const numbers = new Set<number>();
|
||||
for (const attachment of attachments) {
|
||||
if (attachment.kind === "github_pr") {
|
||||
numbers.add(attachment.item.number);
|
||||
}
|
||||
}
|
||||
return numbers;
|
||||
}
|
||||
|
||||
function pruneDismissedCheckoutHintPrNumbers(
|
||||
dismissed: ReadonlySet<number>,
|
||||
attached: ReadonlySet<number>,
|
||||
): ReadonlySet<number> {
|
||||
let changed = false;
|
||||
const next = new Set<number>();
|
||||
for (const prNumber of dismissed) {
|
||||
if (attached.has(prNumber)) {
|
||||
next.add(prNumber);
|
||||
} else {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed ? next : dismissed;
|
||||
}
|
||||
|
||||
function useCheckoutHintDismissals(attachments: ReadonlyArray<UserComposerAttachment>) {
|
||||
const [dismissedPrNumbers, setDismissedPrNumbers] = useState<ReadonlySet<number>>(
|
||||
() => new Set(),
|
||||
);
|
||||
const attachedPrNumbers = useMemo(() => collectAttachedPrNumbers(attachments), [attachments]);
|
||||
|
||||
useEffect(() => {
|
||||
setDismissedPrNumbers((current) =>
|
||||
pruneDismissedCheckoutHintPrNumbers(current, attachedPrNumbers),
|
||||
);
|
||||
}, [attachedPrNumbers]);
|
||||
|
||||
return [dismissedPrNumbers, setDismissedPrNumbers] as const;
|
||||
}
|
||||
|
||||
function usePendingWorkspaceDraftSetup(
|
||||
draftId: string | undefined,
|
||||
): PendingWorkspaceDraftSetup | null {
|
||||
@@ -1647,7 +1538,6 @@ export function NewWorkspaceScreen({
|
||||
typeof normalizeWorkspaceDescriptor
|
||||
> | null>(null);
|
||||
const [pendingAction, setPendingAction] = useState<"chat" | "empty" | null>(null);
|
||||
const [manualPickerSelection, setManualPickerSelection] = useState<PickerSelection | null>(null);
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const [projectPickerOpen, setProjectPickerOpen] = useState(false);
|
||||
const openAddProjectPicker = useOpenAddProject();
|
||||
@@ -1718,10 +1608,22 @@ export function NewWorkspaceScreen({
|
||||
}),
|
||||
});
|
||||
const composerState = chatDraft.composerState;
|
||||
const [dismissedCheckoutHintPrNumbers, setDismissedCheckoutHintPrNumbers] =
|
||||
useCheckoutHintDismissals(chatDraft.attachments);
|
||||
const [pickerSelection, dispatchPickerSelection] = useReducer(
|
||||
reducePickerSelection,
|
||||
initialPickerSelectionState,
|
||||
);
|
||||
const selectedItem = pickerSelection.selectedItem;
|
||||
|
||||
const selectedItem = getSelectedPickerItem(manualPickerSelection);
|
||||
const handleGithubPrDetected = useCallback(() => {
|
||||
dispatchPickerSelection({ type: "pr-detected" });
|
||||
}, []);
|
||||
|
||||
const handleGithubPrAutoAttach = useCallback((item: ForgeSearchItem) => {
|
||||
dispatchPickerSelection({
|
||||
type: "pr-added",
|
||||
item: { kind: "github-pr", item },
|
||||
});
|
||||
}, []);
|
||||
|
||||
const withConnectedClient = useCallback(() => {
|
||||
if (!client || !isConnected) {
|
||||
@@ -1822,7 +1724,7 @@ export function NewWorkspaceScreen({
|
||||
item,
|
||||
});
|
||||
|
||||
setManualPickerSelection({ item });
|
||||
dispatchPickerSelection({ type: "picker-selected", item });
|
||||
chatDraft.setAttachments(nextAttachments);
|
||||
setPickerOpen(false);
|
||||
},
|
||||
@@ -1838,7 +1740,7 @@ export function NewWorkspaceScreen({
|
||||
[itemById, selectPickerItem],
|
||||
);
|
||||
|
||||
const clearManualPickerSelectionForTargetChange = useCallback(
|
||||
const clearPickerSelectionForTargetChange = useCallback(
|
||||
(currentTargetId: string, nextTargetId: string) => {
|
||||
const nextAttachments = clearPickerPrAttachmentForTargetChange({
|
||||
attachments: chatDraft.attachments,
|
||||
@@ -1847,7 +1749,7 @@ export function NewWorkspaceScreen({
|
||||
});
|
||||
if (nextAttachments === chatDraft.attachments) return;
|
||||
chatDraft.setAttachments(nextAttachments);
|
||||
setManualPickerSelection(null);
|
||||
dispatchPickerSelection({ type: "target-changed" });
|
||||
},
|
||||
[chatDraft],
|
||||
);
|
||||
@@ -1859,17 +1761,17 @@ export function NewWorkspaceScreen({
|
||||
// canCreateWorktree or non-git projects become unselectable.
|
||||
selectProjectOption(id);
|
||||
setProjectPickerOpen(false);
|
||||
clearManualPickerSelectionForTargetChange(selectedProjectOptionId, id);
|
||||
clearPickerSelectionForTargetChange(selectedProjectOptionId, id);
|
||||
},
|
||||
[clearManualPickerSelectionForTargetChange, selectProjectOption, selectedProjectOptionId],
|
||||
[clearPickerSelectionForTargetChange, selectProjectOption, selectedProjectOptionId],
|
||||
);
|
||||
|
||||
const handleSelectWorkspaceHost = useCallback(
|
||||
(id: string) => {
|
||||
handleSelectHost(id);
|
||||
clearManualPickerSelectionForTargetChange(selectedServerId, id);
|
||||
clearPickerSelectionForTargetChange(selectedServerId, id);
|
||||
},
|
||||
[clearManualPickerSelectionForTargetChange, handleSelectHost, selectedServerId],
|
||||
[clearPickerSelectionForTargetChange, handleSelectHost, selectedServerId],
|
||||
);
|
||||
|
||||
const handleAddProject = useCallback(() => {
|
||||
@@ -1877,32 +1779,6 @@ export function NewWorkspaceScreen({
|
||||
openAddProjectPicker(selectedServerId);
|
||||
}, [openAddProjectPicker, selectedServerId]);
|
||||
|
||||
const checkoutHintPrAttachment = useMemo(
|
||||
() =>
|
||||
findCheckoutHintPrAttachment({
|
||||
attachments: chatDraft.attachments,
|
||||
selectedItem,
|
||||
dismissedPrNumbers: dismissedCheckoutHintPrNumbers,
|
||||
}),
|
||||
[chatDraft.attachments, dismissedCheckoutHintPrNumbers, selectedItem],
|
||||
);
|
||||
|
||||
const acceptCheckoutHint = useCallback(() => {
|
||||
if (!checkoutHintPrAttachment) return;
|
||||
selectPickerItem({ kind: "github-pr", item: checkoutHintPrAttachment.item });
|
||||
}, [checkoutHintPrAttachment, selectPickerItem]);
|
||||
|
||||
const dismissCheckoutHint = useCallback(() => {
|
||||
if (!checkoutHintPrAttachment) return;
|
||||
const prNumber = checkoutHintPrAttachment.item.number;
|
||||
setDismissedCheckoutHintPrNumbers((current) => {
|
||||
if (current.has(prNumber)) return current;
|
||||
const next = new Set(current);
|
||||
next.add(prNumber);
|
||||
return next;
|
||||
});
|
||||
}, [checkoutHintPrAttachment, setDismissedCheckoutHintPrNumbers]);
|
||||
|
||||
const openPicker = useCallback(() => {
|
||||
setPickerOpen(true);
|
||||
}, []);
|
||||
@@ -2234,42 +2110,11 @@ export function NewWorkspaceScreen({
|
||||
});
|
||||
|
||||
const composerFooter = useMemo(
|
||||
() => (
|
||||
<>
|
||||
{agentControlsWithDisabled ? (
|
||||
<DraftAgentModeControl placement="footer" {...agentControlsWithDisabled} />
|
||||
) : null}
|
||||
{checkoutHintPrAttachment ? (
|
||||
<CheckoutHintBadge
|
||||
label={t("newWorkspace.refPicker.checkoutHint", {
|
||||
number: checkoutHintPrAttachment.item.number,
|
||||
...getCheckoutHintPresentation(checkoutHintPrAttachment.item),
|
||||
})}
|
||||
acceptLabel={t("newWorkspace.refPicker.checkoutPr", {
|
||||
number: checkoutHintPrAttachment.item.number,
|
||||
...getCheckoutHintPresentation(checkoutHintPrAttachment.item),
|
||||
})}
|
||||
dismissLabel={t("newWorkspace.refPicker.dismissCheckoutHint", {
|
||||
number: checkoutHintPrAttachment.item.number,
|
||||
...getCheckoutHintPresentation(checkoutHintPrAttachment.item),
|
||||
})}
|
||||
onAccept={acceptCheckoutHint}
|
||||
onDismiss={dismissCheckoutHint}
|
||||
iconColor={theme.colors.foregroundMuted}
|
||||
iconSize={theme.iconSize.sm}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
[
|
||||
acceptCheckoutHint,
|
||||
agentControlsWithDisabled,
|
||||
checkoutHintPrAttachment,
|
||||
dismissCheckoutHint,
|
||||
t,
|
||||
theme.colors.foregroundMuted,
|
||||
theme.iconSize.sm,
|
||||
],
|
||||
() =>
|
||||
agentControlsWithDisabled ? (
|
||||
<DraftAgentModeControl placement="footer" {...agentControlsWithDisabled} />
|
||||
) : null,
|
||||
[agentControlsWithDisabled],
|
||||
);
|
||||
const screenHeaderLeft = useMemo(() => <SidebarMenuToggle />, []);
|
||||
|
||||
@@ -2294,6 +2139,7 @@ export function NewWorkspaceScreen({
|
||||
submitButtonTestID="workspace-create-submit"
|
||||
submitIcon="return"
|
||||
isSubmitLoading={isPending}
|
||||
waitForGithubAutoAttachOnSubmit
|
||||
submitBehavior="preserve-and-lock"
|
||||
blurOnSubmit={true}
|
||||
value={chatDraft.text}
|
||||
@@ -2301,6 +2147,8 @@ export function NewWorkspaceScreen({
|
||||
attachments={chatDraft.attachments}
|
||||
attachmentScopeKeys={visibleDraftContextScopeKeys}
|
||||
onChangeAttachments={chatDraft.setAttachments}
|
||||
onGithubPrDetected={handleGithubPrDetected}
|
||||
onGithubPrAutoAttach={handleGithubPrAutoAttach}
|
||||
cwd={selectedSourceDirectory ?? ""}
|
||||
clearDraft={handleClearDraft}
|
||||
autoFocus
|
||||
@@ -2387,23 +2235,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
borderRadius: theme.borderRadius["2xl"],
|
||||
gap: theme.spacing[1],
|
||||
},
|
||||
checkoutHintBadge: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
height: BADGE_HEIGHT,
|
||||
maxWidth: 240,
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
borderRadius: theme.borderRadius["2xl"],
|
||||
gap: theme.spacing[1],
|
||||
backgroundColor: theme.colors.surface1,
|
||||
},
|
||||
checkoutHintAction: {
|
||||
width: theme.iconSize.md,
|
||||
height: theme.iconSize.md,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: theme.borderRadius.full,
|
||||
},
|
||||
badgeHovered: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user