- Replace the inline composer PR menu with a dedicated `PullRequestThreadDialog` - Remove `/checkout-pr` from composer slash-command parsing and tests - Fix branch selector virtualization sizing for the checkout-PR row
Unified · Split
| @@ -291,7 +291,8 @@ export function BranchToolbarBranchSelector({ · 290 unmodified lines | ||
| 291 | 291 | const branchListScrollElementRef = useRef<HTMLDivElement | null>(null); |
| 292 | 292 | const branchListVirtualizer = useVirtualizer({ |
| 293 | 293 | count: filteredBranchPickerItems.length, |
| 294 | − estimateSize: () => 28, | |
| 294 | + estimateSize: (index) => | |
| 295 | + filteredBranchPickerItems[index] === checkoutPullRequestItemValue ? 44 : 28, | |
| 295 | 296 | getScrollElement: () => branchListScrollElementRef.current, |
| 296 | 297 | overscan: 12, |
| 297 | 298 | enabled: isBranchMenuOpen && shouldVirtualizeBranchList, |
| @@ -409,7 +410,7 @@ export function BranchToolbarBranchSelector({ · 111 unmodified lines | ||
| 409 | 410 | items={branchPickerItems} |
| 410 | 411 | filteredItems={filteredBranchPickerItems} |
| 411 | 412 | autoHighlight |
| 412 | − virtualized | |
| 413 | + virtualized={shouldVirtualizeBranchList} | |
| 413 | 414 | onItemHighlighted={(_value, eventDetails) => { |
| 414 | 415 | if (!isBranchMenuOpen || eventDetails.index < 0) return; |
| 415 | 416 | branchListVirtualizer.scrollToIndex(eventDetails.index, { align: "auto" }); |
| @@ -119,7 +119,6 @@ import { · 118 unmodified lines | ||
| 119 | 119 | type TurnDiffTreeNode, |
| 120 | 120 | } from "../lib/turnDiffTree"; |
| 121 | 121 | import BranchToolbar from "./BranchToolbar"; |
| 122 | −import { ComposerPullRequestCommandMenu } from "./ComposerPullRequestCommandMenu"; | |
| 123 | 122 | import GitActionsControl from "./GitActionsControl"; |
| 124 | 123 | import { |
| 125 | 124 | isOpenFavoriteEditorShortcut, |
| @@ -217,6 +216,7 @@ import { shouldUseCompactComposerFooter } from "./composerFooterLayout"; · 91 unmodified lines | ||
| 217 | 216 | import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; |
| 218 | 217 | import { clamp } from "effect/Number"; |
| 219 | 218 | import { ComposerPromptEditor, type ComposerPromptEditorHandle } from "./ComposerPromptEditor"; |
| 219 | +import { PullRequestThreadDialog } from "./PullRequestThreadDialog"; | |
| 220 | 220 | import { estimateTimelineMessageHeight } from "./timelineHeight"; |
| 221 | 221 | |
| 222 | 222 | function formatMessageMeta(createdAt: string, duration: string | null): string { |
| @@ -408,7 +408,7 @@ type ComposerCommandItem = · 185 unmodified lines | ||
| 408 | 408 | |
| 409 | 409 | type SendPhase = "idle" | "preparing-worktree" | "sending-turn"; |
| 410 | 410 | |
| 411 | −interface PullRequestComposerMenuState { | |
| 411 | +interface PullRequestDialogState { | |
| 412 | 412 | initialReference: string | null; |
| 413 | 413 | key: number; |
| 414 | 414 | } |
| @@ -657,8 +657,9 @@ export default function ChatView({ threadId }: ChatViewProps) { · 242 unmodified lines | ||
| 657 | 657 | const [nowTick, setNowTick] = useState(() => Date.now()); |
| 658 | 658 | const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0); |
| 659 | 659 | const [composerHighlightedItemId, setComposerHighlightedItemId] = useState<string | null>(null); |
| 660 | − const [pullRequestComposerMenuState, setPullRequestComposerMenuState] = | |
| 661 | − useState<PullRequestComposerMenuState | null>(null); | |
| 660 | + const [pullRequestDialogState, setPullRequestDialogState] = useState<PullRequestDialogState | null>( | |
| 661 | + null, | |
| 662 | + ); | |
| 662 | 663 | const [attachmentPreviewHandoffByMessageId, setAttachmentPreviewHandoffByMessageId] = useState< |
| 663 | 664 | Record<string, string[]> |
| 664 | 665 | >({}); |
| @@ -764,19 +765,19 @@ export default function ChatView({ threadId }: ChatViewProps) { · 99 unmodified lines | ||
| 764 | 765 | const latestTurnSettled = isLatestTurnSettled(activeLatestTurn, activeThread?.session ?? null); |
| 765 | 766 | const activeProject = projects.find((p) => p.id === activeThread?.projectId); |
| 766 | 767 | |
| 767 | − const openPullRequestComposerMenu = useCallback((reference?: string) => { | |
| 768 | + const openPullRequestDialog = useCallback((reference?: string) => { | |
| 768 | 769 | if (!canCheckoutPullRequestIntoThread) { |
| 769 | 770 | return; |
| 770 | 771 | } |
| 771 | − setPullRequestComposerMenuState({ | |
| 772 | + setPullRequestDialogState({ | |
| 772 | 773 | initialReference: reference ?? null, |
| 773 | 774 | key: Date.now(), |
| 774 | 775 | }); |
| 775 | 776 | setComposerHighlightedItemId(null); |
| 776 | 777 | }, [canCheckoutPullRequestIntoThread]); |
| 777 | 778 | |
| 778 | − const closePullRequestComposerMenu = useCallback(() => { | |
| 779 | − setPullRequestComposerMenuState(null); | |
| 779 | + const closePullRequestDialog = useCallback(() => { | |
| 780 | + setPullRequestDialogState(null); | |
| 780 | 781 | }, []); |
| 781 | 782 | |
| 782 | 783 | const openOrReuseProjectDraftThread = useCallback( |
| @@ -1254,7 +1255,6 @@ export default function ChatView({ threadId }: ChatViewProps) { · 471 unmodified lines | ||
| 1254 | 1255 | timelineEntries, |
| 1255 | 1256 | ]); |
| 1256 | 1257 | const gitCwd = activeThread?.worktreePath ?? activeProject?.cwd ?? null; |
| 1257 | − const pullRequestComposerMenuOpen = pullRequestComposerMenuState !== null; | |
| 1258 | 1258 | const composerTriggerKind = composerTrigger?.kind ?? null; |
| 1259 | 1259 | const pathTriggerQuery = composerTrigger?.kind === "path" ? composerTrigger.query : ""; |
| 1260 | 1260 | const isPathTrigger = composerTriggerKind === "path"; |
| @@ -1311,22 +1311,12 @@ export default function ChatView({ threadId }: ChatViewProps) { · 50 unmodified lines | ||
| 1311 | 1311 | label: "/default", |
| 1312 | 1312 | description: "Switch this thread back to normal chat mode", |
| 1313 | 1313 | }, |
| 1314 | − { | |
| 1315 | − id: "slash:checkout-pr", | |
| 1316 | − type: "slash-command", | |
| 1317 | − command: "checkout-pr", | |
| 1318 | − label: "/checkout-pr", | |
| 1319 | − description: "Create a new thread from a GitHub pull request", | |
| 1320 | − }, | |
| 1321 | 1314 | ] satisfies ReadonlyArray<Extract<ComposerCommandItem, { type: "slash-command" }>>; |
| 1322 | − const availableSlashCommandItems = canCheckoutPullRequestIntoThread | |
| 1323 | − ? slashCommandItems | |
| 1324 | − : slashCommandItems.filter((item) => item.command !== "checkout-pr"); | |
| 1325 | 1315 | const query = composerTrigger.query.trim().toLowerCase(); |
| 1326 | 1316 | if (!query) { |
| 1327 | − return [...availableSlashCommandItems]; | |
| 1317 | + return [...slashCommandItems]; | |
| 1328 | 1318 | } |
| 1329 | − return availableSlashCommandItems.filter( | |
| 1319 | + return slashCommandItems.filter( | |
| 1330 | 1320 | (item) => item.command.includes(query) || item.label.slice(1).includes(query), |
| 1331 | 1321 | ); |
| 1332 | 1322 | } |
| @@ -1347,16 +1337,14 @@ export default function ChatView({ threadId }: ChatViewProps) { · 14 unmodified lines | ||
| 1347 | 1337 | label: name, |
| 1348 | 1338 | description: `${providerLabel} · ${slug}`, |
| 1349 | 1339 | })); |
| 1350 | − }, [canCheckoutPullRequestIntoThread, composerTrigger, searchableModelOptions, workspaceEntries]); | |
| 1351 | − const composerMenuOpen = Boolean(composerTrigger) || pullRequestComposerMenuOpen; | |
| 1340 | + }, [composerTrigger, searchableModelOptions, workspaceEntries]); | |
| 1341 | + const composerMenuOpen = Boolean(composerTrigger); | |
| 1352 | 1342 | const activeComposerMenuItem = useMemo( |
| 1353 | 1343 | () => |
| 1354 | − pullRequestComposerMenuOpen | |
| 1355 | − ? null | |
| 1356 | − : (composerMenuItems.find((item) => item.id === composerHighlightedItemId) ?? | |
| 1357 | − composerMenuItems[0] ?? | |
| 1358 | − null), | |
| 1359 | − [composerHighlightedItemId, composerMenuItems, pullRequestComposerMenuOpen], | |
| 1344 | + composerMenuItems.find((item) => item.id === composerHighlightedItemId) ?? | |
| 1345 | + composerMenuItems[0] ?? | |
| 1346 | + null, | |
| 1347 | + [composerHighlightedItemId, composerMenuItems], | |
| 1360 | 1348 | ); |
| 1361 | 1349 | composerMenuOpenRef.current = composerMenuOpen; |
| 1362 | 1350 | composerMenuItemsRef.current = composerMenuItems; |
| @@ -2055,7 +2043,7 @@ export default function ChatView({ threadId }: ChatViewProps) { · 692 unmodified lines | ||
| 2055 | 2043 | |
| 2056 | 2044 | useEffect(() => { |
| 2057 | 2045 | setExpandedWorkGroups({}); |
| 2058 | − setPullRequestComposerMenuState(null); | |
| 2046 | + setPullRequestDialogState(null); | |
| 2059 | 2047 | if (planSidebarOpenOnNextThreadRef.current) { |
| 2060 | 2048 | planSidebarOpenOnNextThreadRef.current = false; |
| 2061 | 2049 | setPlanSidebarOpen(true); |
| @@ -2070,16 +2058,12 @@ export default function ChatView({ threadId }: ChatViewProps) { · 8 unmodified lines | ||
| 2070 | 2058 | setComposerHighlightedItemId(null); |
| 2071 | 2059 | return; |
| 2072 | 2060 | } |
| 2073 | − if (pullRequestComposerMenuOpen) { | |
| 2074 | − setComposerHighlightedItemId(null); | |
| 2075 | − return; | |
| 2076 | − } | |
| 2077 | 2061 | setComposerHighlightedItemId((existing) => |
| 2078 | 2062 | existing && composerMenuItems.some((item) => item.id === existing) |
| 2079 | 2063 | ? existing |
| 2080 | 2064 | : (composerMenuItems[0]?.id ?? null), |
| 2081 | 2065 | ); |
| 2082 | − }, [composerMenuItems, composerMenuOpen, pullRequestComposerMenuOpen]); | |
| 2066 | + }, [composerMenuItems, composerMenuOpen]); | |
| 2083 | 2067 | |
| 2084 | 2068 | useEffect(() => { |
| 2085 | 2069 | setIsRevertingCheckpoint(false); |
| @@ -2589,15 +2573,8 @@ export default function ChatView({ threadId }: ChatViewProps) { · 503 unmodified lines | ||
| 2589 | 2573 | } |
| 2590 | 2574 | const standaloneSlashCommand = |
| 2591 | 2575 | composerImages.length === 0 ? parseStandaloneComposerSlashCommand(trimmed) : null; |
| 2592 | − const shouldHandleStandaloneSlashCommand = | |
| 2593 | − standaloneSlashCommand !== null && | |
| 2594 | − (standaloneSlashCommand !== "checkout-pr" || canCheckoutPullRequestIntoThread); | |
| 2595 | − if (shouldHandleStandaloneSlashCommand && standaloneSlashCommand) { | |
| 2596 | − if (standaloneSlashCommand === "checkout-pr") { | |
| 2597 | − openPullRequestComposerMenu(); | |
| 2598 | − } else { | |
| 2599 | − await handleInteractionModeChange(standaloneSlashCommand); | |
| 2600 | − } | |
| 2576 | + if (standaloneSlashCommand) { | |
| 2577 | + await handleInteractionModeChange(standaloneSlashCommand); | |
| 2601 | 2578 | promptRef.current = ""; |
| 2602 | 2579 | clearComposerDraftContent(activeThread.id); |
| 2603 | 2580 | setComposerHighlightedItemId(null); |
| @@ -3386,11 +3363,7 @@ export default function ChatView({ threadId }: ChatViewProps) { · 782 unmodified lines | ||
| 3386 | 3363 | } |
| 3387 | 3364 | return; |
| 3388 | 3365 | } |
| 3389 | − if (item.command === "checkout-pr") { | |
| 3390 | − openPullRequestComposerMenu(); | |
| 3391 | − } else { | |
| 3392 | − void handleInteractionModeChange(item.command === "plan" ? "plan" : "default"); | |
| 3393 | − } | |
| 3366 | + void handleInteractionModeChange(item.command === "plan" ? "plan" : "default"); | |
| 3394 | 3367 | const applied = applyPromptReplacement(trigger.rangeStart, trigger.rangeEnd, "", { |
| 3395 | 3368 | expectedText: expectedToken, |
| 3396 | 3369 | }); |
| @@ -3410,7 +3383,6 @@ export default function ChatView({ threadId }: ChatViewProps) { · 13 unmodified lines | ||
| 3410 | 3383 | [ |
| 3411 | 3384 | applyPromptReplacement, |
| 3412 | 3385 | handleInteractionModeChange, |
| 3413 | − openPullRequestComposerMenu, | |
| 3414 | 3386 | onProviderModelSelect, |
| 3415 | 3387 | resolveActiveComposerTrigger, |
| 3416 | 3388 | ], |
| @@ -3485,12 +3457,6 @@ export default function ChatView({ threadId }: ChatViewProps) { · 68 unmodified lines | ||
| 3485 | 3457 | const { trigger } = resolveActiveComposerTrigger(); |
| 3486 | 3458 | const menuIsActive = composerMenuOpenRef.current || trigger !== null; |
| 3487 | 3459 | |
| 3488 | − if (pullRequestComposerMenuOpen) { | |
| 3489 | − if (key === "ArrowDown" || key === "ArrowUp" || key === "Tab" || key === "Enter") { | |
| 3490 | − return true; | |
| 3491 | − } | |
| 3492 | − } | |
| 3493 | − | |
| 3494 | 3460 | if (menuIsActive) { |
| 3495 | 3461 | const currentItems = composerMenuItemsRef.current; |
| 3496 | 3462 | if (key === "ArrowDown" && currentItems.length > 0) { |
| @@ -3712,28 +3678,15 @@ export default function ChatView({ threadId }: ChatViewProps) { · 215 unmodified lines | ||
| 3712 | 3678 | > |
| 3713 | 3679 | {composerMenuOpen && !isComposerApprovalState && ( |
| 3714 | 3680 | <div className="absolute inset-x-0 bottom-full z-20 mb-2 px-1"> |
| 3715 | − {pullRequestComposerMenuState ? ( | |
| 3716 | − <ComposerPullRequestCommandMenu | |
| 3717 | − key={pullRequestComposerMenuState.key} | |
| 3718 | − cwd={activeProject?.cwd ?? null} | |
| 3719 | − initialReference={pullRequestComposerMenuState.initialReference} | |
| 3720 | − onCancel={closePullRequestComposerMenu} | |
| 3721 | − onPrepared={async (input) => { | |
| 3722 | − await handlePreparedPullRequestThread(input); | |
| 3723 | − closePullRequestComposerMenu(); | |
| 3724 | − }} | |
| 3725 | − /> | |
| 3726 | − ) : ( | |
| 3727 | − <ComposerCommandMenu | |
| 3728 | − items={composerMenuItems} | |
| 3729 | − resolvedTheme={resolvedTheme} | |
| 3730 | − isLoading={isComposerMenuLoading} | |
| 3731 | − triggerKind={composerTriggerKind} | |
| 3732 | − activeItemId={activeComposerMenuItem?.id ?? null} | |
| 3733 | − onHighlightedItemChange={onComposerMenuItemHighlighted} | |
| 3734 | − onSelect={onSelectComposerItem} | |
| 3735 | − /> | |
| 3736 | − )} | |
| 3681 | + <ComposerCommandMenu | |
| 3682 | + items={composerMenuItems} | |
| 3683 | + resolvedTheme={resolvedTheme} | |
| 3684 | + isLoading={isComposerMenuLoading} | |
| 3685 | + triggerKind={composerTriggerKind} | |
| 3686 | + activeItemId={activeComposerMenuItem?.id ?? null} | |
| 3687 | + onHighlightedItemChange={onComposerMenuItemHighlighted} | |
| 3688 | + onSelect={onSelectComposerItem} | |
| 3689 | + /> | |
| 3737 | 3690 | </div> |
| 3738 | 3691 | )} |
| 3739 | 3692 | |
| @@ -4160,10 +4113,24 @@ export default function ChatView({ threadId }: ChatViewProps) { · 420 unmodified lines | ||
| 4160 | 4113 | envLocked={envLocked} |
| 4161 | 4114 | onComposerFocusRequest={scheduleComposerFocus} |
| 4162 | 4115 | {...(canCheckoutPullRequestIntoThread |
| 4163 | − ? { onCheckoutPullRequestRequest: openPullRequestComposerMenu } | |
| 4116 | + ? { onCheckoutPullRequestRequest: openPullRequestDialog } | |
| 4164 | 4117 | : {})} |
| 4165 | 4118 | /> |
| 4166 | 4119 | )} |
| 4120 | + {pullRequestDialogState ? ( | |
| 4121 | + <PullRequestThreadDialog | |
| 4122 | + key={pullRequestDialogState.key} | |
| 4123 | + open | |
| 4124 | + cwd={activeProject?.cwd ?? null} | |
| 4125 | + initialReference={pullRequestDialogState.initialReference} | |
| 4126 | + onOpenChange={(open) => { | |
| 4127 | + if (!open) { | |
| 4128 | + closePullRequestDialog(); | |
| 4129 | + } | |
| 4130 | + }} | |
| 4131 | + onPrepared={handlePreparedPullRequestThread} | |
| 4132 | + /> | |
| 4133 | + ) : null} | |
| 4167 | 4134 | </div>{/* end chat column */} |
| 4168 | 4135 | |
| 4169 | 4136 | {/* Plan sidebar */} |
| @@ -10,22 +10,33 @@ import { · 9 unmodified lines | ||
| 10 | 10 | import { cn } from "~/lib/utils"; |
| 11 | 11 | import { parsePullRequestReference } from "~/pullRequestReference"; |
| 12 | 12 | import { Button } from "./ui/button"; |
| 13 | +import { | |
| 14 | + Dialog, | |
| 15 | + DialogDescription, | |
| 16 | + DialogFooter, | |
| 17 | + DialogHeader, | |
| 18 | + DialogPanel, | |
| 19 | + DialogPopup, | |
| 20 | + DialogTitle, | |
| 21 | +} from "./ui/dialog"; | |
| 13 | 22 | import { Input } from "./ui/input"; |
| 14 | 23 | import { Spinner } from "./ui/spinner"; |
| 15 | 24 | |
| 16 | −interface ComposerPullRequestCommandMenuProps { | |
| 25 | +interface PullRequestThreadDialogProps { | |
| 26 | + open: boolean; | |
| 17 | 27 | cwd: string | null; |
| 18 | 28 | initialReference: string | null; |
| 19 | − onCancel: () => void; | |
| 29 | + onOpenChange: (open: boolean) => void; | |
| 20 | 30 | onPrepared: (input: { branch: string; worktreePath: string | null }) => Promise<void> | void; |
| 21 | 31 | } |
| 22 | 32 | |
| 23 | −export function ComposerPullRequestCommandMenu({ | |
| 33 | +export function PullRequestThreadDialog({ | |
| 34 | + open, | |
| 24 | 35 | cwd, |
| 25 | 36 | initialReference, |
| 26 | − onCancel, | |
| 37 | + onOpenChange, | |
| 27 | 38 | onPrepared, |
| 28 | −}: ComposerPullRequestCommandMenuProps) { | |
| 39 | +}: PullRequestThreadDialogProps) { | |
| 29 | 40 | const queryClient = useQueryClient(); |
| 30 | 41 | const referenceInputRef = useRef<HTMLInputElement>(null); |
| 31 | 42 | const [reference, setReference] = useState(initialReference ?? ""); |
| @@ -38,12 +49,14 @@ export function ComposerPullRequestCommandMenu({ · 6 unmodified lines | ||
| 38 | 49 | ); |
| 39 | 50 | |
| 40 | 51 | useEffect(() => { |
| 52 | + if (!open) return; | |
| 41 | 53 | setReference(initialReference ?? ""); |
| 42 | 54 | setReferenceDirty(false); |
| 43 | 55 | setPreparingMode(null); |
| 44 | − }, [initialReference]); | |
| 56 | + }, [initialReference, open]); | |
| 45 | 57 | |
| 46 | 58 | useEffect(() => { |
| 59 | + if (!open) return; | |
| 47 | 60 | const frame = window.requestAnimationFrame(() => { |
| 48 | 61 | referenceInputRef.current?.focus(); |
| 49 | 62 | referenceInputRef.current?.select(); |
| @@ -51,14 +64,14 @@ export function ComposerPullRequestCommandMenu({ · 1 unmodified line | ||
| 51 | 64 | return () => { |
| 52 | 65 | window.cancelAnimationFrame(frame); |
| 53 | 66 | }; |
| 54 | − }, []); | |
| 67 | + }, [open]); | |
| 55 | 68 | |
| 56 | 69 | const parsedReference = parsePullRequestReference(reference); |
| 57 | 70 | const parsedDebouncedReference = parsePullRequestReference(debouncedReference); |
| 58 | 71 | const resolvePullRequestQuery = useQuery( |
| 59 | 72 | gitResolvePullRequestQueryOptions({ |
| 60 | 73 | cwd, |
| 61 | − reference: parsedDebouncedReference, | |
| 74 | + reference: open ? parsedDebouncedReference : null, | |
| 62 | 75 | }), |
| 63 | 76 | ); |
| 64 | 77 | const cachedPullRequest = useMemo(() => { |
| @@ -83,6 +96,7 @@ export function ComposerPullRequestCommandMenu({ · 18 unmodified lines | ||
| 83 | 96 | : null; |
| 84 | 97 | const resolvedPullRequest = liveResolvedPullRequest ?? cachedPullRequest; |
| 85 | 98 | const isResolving = |
| 99 | + open && | |
| 86 | 100 | parsedReference !== null && |
| 87 | 101 | resolvedPullRequest === null && |
| 88 | 102 | (referenceDebouncer.state.isPending || |
| @@ -120,11 +134,13 @@ export function ComposerPullRequestCommandMenu({ · 31 unmodified lines | ||
| 120 | 134 | branch: result.branch, |
| 121 | 135 | worktreePath: result.worktreePath, |
| 122 | 136 | }); |
| 137 | + onOpenChange(false); | |
| 123 | 138 | } finally { |
| 124 | 139 | setPreparingMode(null); |
| 125 | 140 | } |
| 126 | 141 | }, [ |
| 127 | 142 | cwd, |
| 143 | + onOpenChange, | |
| 128 | 144 | onPrepared, |
| 129 | 145 | parsedReference, |
| 130 | 146 | preparePullRequestThreadMutation, |
| @@ -135,10 +151,10 @@ export function ComposerPullRequestCommandMenu({ · 4 unmodified lines | ||
| 135 | 151 | !referenceDirty |
| 136 | 152 | ? null |
| 137 | 153 | : reference.trim().length === 0 |
| 138 | − ? "Paste a GitHub pull request URL or enter 123 / #123." | |
| 139 | − : parsedReference === null | |
| 140 | − ? "Use a GitHub pull request URL, 123, or #123." | |
| 141 | − : null; | |
| 154 | + ? "Paste a GitHub pull request URL or enter 123 / #123." | |
| 155 | + : parsedReference === null | |
| 156 | + ? "Use a GitHub pull request URL, 123, or #123." | |
| 157 | + : null; | |
| 142 | 158 | const errorMessage = |
| 143 | 159 | validationMessage ?? |
| 144 | 160 | (resolvedPullRequest === null && resolvePullRequestQuery.isError |
| @@ -152,73 +168,79 @@ export function ComposerPullRequestCommandMenu({ · 7 unmodified lines | ||
| 152 | 168 | : null)); |
| 153 | 169 | |
| 154 | 170 | return ( |
| 155 | − <div className="overflow-hidden rounded-xl border border-border/80 bg-popover/96 shadow-lg/8 backdrop-blur-xs"> | |
| 156 | − <div className="space-y-4 p-3"> | |
| 157 | − <div className="space-y-2"> | |
| 158 | − <div className="flex items-center justify-between gap-3"> | |
| 159 | − <div> | |
| 160 | − <p className="font-medium text-sm">Checkout Pull Request</p> | |
| 161 | − <p className="text-muted-foreground text-xs"> | |
| 162 | − Resolve a PR, then choose Local or Worktree for the new draft thread. | |
| 163 | − </p> | |
| 164 | − </div> | |
| 165 | − </div> | |
| 166 | − <Input | |
| 167 | − autoFocus | |
| 168 | − ref={referenceInputRef} | |
| 169 | − placeholder="https://github.com/owner/repo/pull/42 or #42" | |
| 170 | − value={reference} | |
| 171 | − onChange={(event) => { | |
| 172 | − setReferenceDirty(true); | |
| 173 | − setReference(event.target.value); | |
| 174 | − }} | |
| 175 | − onKeyDown={(event) => { | |
| 176 | − if (event.key !== "Enter") { | |
| 177 | − return; | |
| 178 | − } | |
| 179 | − event.preventDefault(); | |
| 180 | − if (!isResolving && !preparePullRequestThreadMutation.isPending) { | |
| 181 | − void handleConfirm("local"); | |
| 182 | − } | |
| 183 | − }} | |
| 184 | − /> | |
| 185 | − </div> | |
| 171 | + <Dialog | |
| 172 | + open={open} | |
| 173 | + onOpenChange={(nextOpen) => { | |
| 174 | + if (!preparePullRequestThreadMutation.isPending) { | |
| 175 | + onOpenChange(nextOpen); | |
| 176 | + } | |
| 177 | + }} | |
| 178 | + > | |
| 179 | + <DialogPopup className="max-w-xl"> | |
| 180 | + <DialogHeader> | |
| 181 | + <DialogTitle>Checkout Pull Request</DialogTitle> | |
| 182 | + <DialogDescription> | |
| 183 | + Resolve a GitHub pull request, then create the draft thread in the main repo or in a | |
| 184 | + dedicated worktree. | |
| 185 | + </DialogDescription> | |
| 186 | + </DialogHeader> | |
| 187 | + <DialogPanel className="space-y-4"> | |
| 188 | + <label className="grid gap-1.5"> | |
| 189 | + <span className="text-xs font-medium text-foreground">Pull request</span> | |
| 190 | + <Input | |
| 191 | + ref={referenceInputRef} | |
| 192 | + placeholder="https://github.com/owner/repo/pull/42 or #42" | |
| 193 | + value={reference} | |
| 194 | + onChange={(event) => { | |
| 195 | + setReferenceDirty(true); | |
| 196 | + setReference(event.target.value); | |
| 197 | + }} | |
| 198 | + onKeyDown={(event) => { | |
| 199 | + if (event.key !== "Enter") { | |
| 200 | + return; | |
| 201 | + } | |
| 202 | + event.preventDefault(); | |
| 203 | + if (!isResolving && !preparePullRequestThreadMutation.isPending) { | |
| 204 | + void handleConfirm("local"); | |
| 205 | + } | |
| 206 | + }} | |
| 207 | + /> | |
| 208 | + </label> | |
| 186 | 209 | |
| 187 | − {resolvedPullRequest ? ( | |
| 188 | − <div className="rounded-xl border border-border/70 bg-muted/24 p-3"> | |
| 189 | − <div className="flex items-start justify-between gap-3"> | |
| 190 | − <div className="min-w-0"> | |
| 191 | − <p className="truncate font-medium text-sm">{resolvedPullRequest.title}</p> | |
| 192 | − <p className="truncate text-muted-foreground text-xs"> | |
| 193 | − #{resolvedPullRequest.number} · {resolvedPullRequest.headBranch} to{" "} | |
| 194 | − {resolvedPullRequest.baseBranch} | |
| 195 | − </p> | |
| 210 | + {resolvedPullRequest ? ( | |
| 211 | + <div className="rounded-xl border border-border/70 bg-muted/24 p-3"> | |
| 212 | + <div className="flex items-start justify-between gap-3"> | |
| 213 | + <div className="min-w-0"> | |
| 214 | + <p className="truncate font-medium text-sm">{resolvedPullRequest.title}</p> | |
| 215 | + <p className="truncate text-muted-foreground text-xs"> | |
| 216 | + #{resolvedPullRequest.number} · {resolvedPullRequest.headBranch} to{" "} | |
| 217 | + {resolvedPullRequest.baseBranch} | |
| 218 | + </p> | |
| 219 | + </div> | |
| 220 | + <span className={cn("shrink-0 text-xs capitalize", statusTone)}> | |
| 221 | + {resolvedPullRequest.state} | |
| 222 | + </span> | |
| 196 | 223 | </div> |
| 197 | − <span className={cn("shrink-0 text-xs capitalize", statusTone)}> | |
| 198 | − {resolvedPullRequest.state} | |
| 199 | − </span> | |
| 200 | 224 | </div> |
| 201 | − </div> | |
| 202 | − ) : null} | |
| 203 | − | |
| 204 | − {isResolving ? ( | |
| 205 | − <div className="flex items-center gap-2 text-muted-foreground text-xs"> | |
| 206 | − <Spinner className="size-3.5" /> | |
| 207 | − Resolving pull request... | |
| 208 | − </div> | |
| 209 | − ) : null} | |
| 225 | + ) : null} | |
| 210 | 226 | |
| 211 | − {errorMessage ? <p className="text-destructive text-xs">{errorMessage}</p> : null} | |
| 212 | − </div> | |
| 227 | + {isResolving ? ( | |
| 228 | + <div className="flex items-center gap-2 text-muted-foreground text-xs"> | |
| 229 | + <Spinner className="size-3.5" /> | |
| 230 | + Resolving pull request... | |
| 231 | + </div> | |
| 232 | + ) : null} | |
| 213 | 233 | |
| 214 | − <div className="flex items-center justify-between gap-2 border-t px-3 py-2"> | |
| 215 | − <p className="text-muted-foreground text-xs"> | |
| 216 | − {resolvedPullRequest | |
| 217 | − ? "Create the draft thread in the main repo or in a dedicated worktree." | |
| 218 | − : "Resolve the pull request first."} | |
| 219 | − </p> | |
| 220 | − <div className="flex items-center gap-2"> | |
| 221 | − <Button type="button" variant="ghost" size="sm" onClick={onCancel}> | |
| 234 | + {errorMessage ? <p className="text-destructive text-xs">{errorMessage}</p> : null} | |
| 235 | + </DialogPanel> | |
| 236 | + <DialogFooter> | |
| 237 | + <Button | |
| 238 | + type="button" | |
| 239 | + variant="outline" | |
| 240 | + size="sm" | |
| 241 | + onClick={() => onOpenChange(false)} | |
| 242 | + disabled={preparePullRequestThreadMutation.isPending} | |
| 243 | + > | |
| 222 | 244 | Cancel |
| 223 | 245 | </Button> |
| 224 | 246 | <Button |
| @@ -252,8 +274,8 @@ export function ComposerPullRequestCommandMenu({ · 27 unmodified lines | ||
| 252 | 274 | > |
| 253 | 275 | {preparingMode === "worktree" ? "Preparing worktree..." : "Worktree"} |
| 254 | 276 | </Button> |
| 255 | − </div> | |
| 256 | − </div> | |
| 257 | − </div> | |
| 277 | + </DialogFooter> | |
| 278 | + </DialogPopup> | |
| 279 | + </Dialog> | |
| 258 | 280 | ); |
| 259 | 281 | } |
| @@ -134,11 +134,11 @@ describe("parseStandaloneComposerSlashCommand", () => { · 133 unmodified lines | ||
| 134 | 134 | expect(parseStandaloneComposerSlashCommand("/default")).toBe("default"); |
| 135 | 135 | }); |
| 136 | 136 | |
| 137 | − it("parses standalone /checkout-pr command", () => { | |
| 138 | − expect(parseStandaloneComposerSlashCommand(" /checkout-pr ")).toBe("checkout-pr"); | |
| 139 | − }); | |
| 140 | − | |
| 141 | 137 | it("ignores slash commands with extra message text", () => { |
| 142 | 138 | expect(parseStandaloneComposerSlashCommand("/plan explain this")).toBeNull(); |
| 143 | 139 | }); |
| 140 | + | |
| 141 | + it("ignores removed /checkout-pr command", () => { | |
| 142 | + expect(parseStandaloneComposerSlashCommand(" /checkout-pr ")).toBeNull(); | |
| 143 | + }); | |
| 144 | 144 | }); |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | import { splitPromptIntoComposerSegments } from "./composer-editor-mentions"; |
| 2 | 2 | |
| 3 | 3 | export type ComposerTriggerKind = "path" | "slash-command" | "slash-model"; |
| 4 | −export type ComposerSlashCommand = "model" | "plan" | "default" | "checkout-pr"; | |
| 4 | +export type ComposerSlashCommand = "model" | "plan" | "default"; | |
| 5 | 5 | |
| 6 | 6 | export interface ComposerTrigger { |
| 7 | 7 | kind: ComposerTriggerKind; |
| @@ -14,7 +14,6 @@ const SLASH_COMMANDS: readonly ComposerSlashCommand[] = [ · 6 unmodified lines | ||
| 14 | 14 | "model", |
| 15 | 15 | "plan", |
| 16 | 16 | "default", |
| 17 | − "checkout-pr", | |
| 18 | 17 | ]; |
| 19 | 18 | |
| 20 | 19 | function clampCursor(text: string, cursor: number): number { |
| @@ -169,13 +168,12 @@ export function parseStandaloneComposerSlashCommand(text: string): Exclude< · 148 unmodified lines | ||
| 169 | 168 | ComposerSlashCommand, |
| 170 | 169 | "model" |
| 171 | 170 | > | null { |
| 172 | − const match = /^\/(plan|default|checkout-pr)\s*$/i.exec(text.trim()); | |
| 171 | + const match = /^\/(plan|default)\s*$/i.exec(text.trim()); | |
| 173 | 172 | if (!match) { |
| 174 | 173 | return null; |
| 175 | 174 | } |
| 176 | 175 | const command = match[1]?.toLowerCase(); |
| 177 | 176 | if (command === "plan") return "plan"; |
| 178 | − if (command === "checkout-pr") return "checkout-pr"; | |
| 179 | 177 | return "default"; |
| 180 | 178 | } |
| 181 | 179 | |