| author | |
| committer | |
| log | d28ced8fd7471302d42e2e272fcbd62f9b1404c8 |
| tree | a0b24c78ef5b0f2940929454255219f07e15b7f1 |
| parent | 076608021c89afb294f4bdc75d99018936540c95 |
- 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 row6 files changed, 336 insertions(+), 348 deletions(-)
apps/web/src/components/BranchToolbarBranchSelector.tsx+3-2| ... | @@ -291,7 +291,8 @@ export function BranchToolbarBranchSelector({ | ... | @@ -291,7 +291,8 @@ export function BranchToolbarBranchSelector({ |
| 291 | const branchListScrollElementRef = useRef<HTMLDivElement | null>(null); | 291 | const branchListScrollElementRef = useRef<HTMLDivElement | null>(null); |
| 292 | const branchListVirtualizer = useVirtualizer({ | 292 | const branchListVirtualizer = useVirtualizer({ |
| 293 | count: filteredBranchPickerItems.length, | 293 | count: filteredBranchPickerItems.length, |
| 294 | estimateSize: () => 28, | 294 | estimateSize: (index) => |
| 295 | filteredBranchPickerItems[index] === checkoutPullRequestItemValue ? 44 : 28, | ||
| 295 | getScrollElement: () => branchListScrollElementRef.current, | 296 | getScrollElement: () => branchListScrollElementRef.current, |
| 296 | overscan: 12, | 297 | overscan: 12, |
| 297 | enabled: isBranchMenuOpen && shouldVirtualizeBranchList, | 298 | enabled: isBranchMenuOpen && shouldVirtualizeBranchList, |
| ... | @@ -409,7 +410,7 @@ export function BranchToolbarBranchSelector({ | ... | @@ -409,7 +410,7 @@ export function BranchToolbarBranchSelector({ |
| 409 | items={branchPickerItems} | 410 | items={branchPickerItems} |
| 410 | filteredItems={filteredBranchPickerItems} | 411 | filteredItems={filteredBranchPickerItems} |
| 411 | autoHighlight | 412 | autoHighlight |
| 412 | virtualized | 413 | virtualized={shouldVirtualizeBranchList} |
| 413 | onItemHighlighted={(_value, eventDetails) => { | 414 | onItemHighlighted={(_value, eventDetails) => { |
| 414 | if (!isBranchMenuOpen || eventDetails.index < 0) return; | 415 | if (!isBranchMenuOpen || eventDetails.index < 0) return; |
| 415 | branchListVirtualizer.scrollToIndex(eventDetails.index, { align: "auto" }); | 416 | branchListVirtualizer.scrollToIndex(eventDetails.index, { align: "auto" }); |
apps/web/src/components/ChatView.tsx+46-79| ... | @@ -119,7 +119,6 @@ import { | ... | @@ -119,7 +119,6 @@ import { |
| 119 | type TurnDiffTreeNode, | 119 | type TurnDiffTreeNode, |
| 120 | } from "../lib/turnDiffTree"; | 120 | } from "../lib/turnDiffTree"; |
| 121 | import BranchToolbar from "./BranchToolbar"; | 121 | import BranchToolbar from "./BranchToolbar"; |
| 122 | import { ComposerPullRequestCommandMenu } from "./ComposerPullRequestCommandMenu"; | ||
| 123 | import GitActionsControl from "./GitActionsControl"; | 122 | import GitActionsControl from "./GitActionsControl"; |
| 124 | import { | 123 | import { |
| 125 | isOpenFavoriteEditorShortcut, | 124 | isOpenFavoriteEditorShortcut, |
| ... | @@ -217,6 +216,7 @@ import { shouldUseCompactComposerFooter } from "./composerFooterLayout"; | ... | @@ -217,6 +216,7 @@ import { shouldUseCompactComposerFooter } from "./composerFooterLayout"; |
| 217 | import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; | 216 | import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; |
| 218 | import { clamp } from "effect/Number"; | 217 | import { clamp } from "effect/Number"; |
| 219 | import { ComposerPromptEditor, type ComposerPromptEditorHandle } from "./ComposerPromptEditor"; | 218 | import { ComposerPromptEditor, type ComposerPromptEditorHandle } from "./ComposerPromptEditor"; |
| 219 | import { PullRequestThreadDialog } from "./PullRequestThreadDialog"; | ||
| 220 | import { estimateTimelineMessageHeight } from "./timelineHeight"; | 220 | import { estimateTimelineMessageHeight } from "./timelineHeight"; |
| 221 | 221 | ||
| 222 | function formatMessageMeta(createdAt: string, duration: string | null): string { | 222 | function formatMessageMeta(createdAt: string, duration: string | null): string { |
| ... | @@ -408,7 +408,7 @@ type ComposerCommandItem = | ... | @@ -408,7 +408,7 @@ type ComposerCommandItem = |
| 408 | 408 | ||
| 409 | type SendPhase = "idle" | "preparing-worktree" | "sending-turn"; | 409 | type SendPhase = "idle" | "preparing-worktree" | "sending-turn"; |
| 410 | 410 | ||
| 411 | interface PullRequestComposerMenuState { | 411 | interface PullRequestDialogState { |
| 412 | initialReference: string | null; | 412 | initialReference: string | null; |
| 413 | key: number; | 413 | key: number; |
| 414 | } | 414 | } |
| ... | @@ -657,8 +657,9 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -657,8 +657,9 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 657 | const [nowTick, setNowTick] = useState(() => Date.now()); | 657 | const [nowTick, setNowTick] = useState(() => Date.now()); |
| 658 | const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0); | 658 | const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0); |
| 659 | const [composerHighlightedItemId, setComposerHighlightedItemId] = useState<string | null>(null); | 659 | const [composerHighlightedItemId, setComposerHighlightedItemId] = useState<string | null>(null); |
| 660 | const [pullRequestComposerMenuState, setPullRequestComposerMenuState] = | 660 | const [pullRequestDialogState, setPullRequestDialogState] = useState<PullRequestDialogState | null>( |
| 661 | useState<PullRequestComposerMenuState | null>(null); | 661 | null, |
| 662 | ); | ||
| 662 | const [attachmentPreviewHandoffByMessageId, setAttachmentPreviewHandoffByMessageId] = useState< | 663 | const [attachmentPreviewHandoffByMessageId, setAttachmentPreviewHandoffByMessageId] = useState< |
| 663 | Record<string, string[]> | 664 | Record<string, string[]> |
| 664 | >({}); | 665 | >({}); |
| ... | @@ -764,19 +765,19 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -764,19 +765,19 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 764 | const latestTurnSettled = isLatestTurnSettled(activeLatestTurn, activeThread?.session ?? null); | 765 | const latestTurnSettled = isLatestTurnSettled(activeLatestTurn, activeThread?.session ?? null); |
| 765 | const activeProject = projects.find((p) => p.id === activeThread?.projectId); | 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 | if (!canCheckoutPullRequestIntoThread) { | 769 | if (!canCheckoutPullRequestIntoThread) { |
| 769 | return; | 770 | return; |
| 770 | } | 771 | } |
| 771 | setPullRequestComposerMenuState({ | 772 | setPullRequestDialogState({ |
| 772 | initialReference: reference ?? null, | 773 | initialReference: reference ?? null, |
| 773 | key: Date.now(), | 774 | key: Date.now(), |
| 774 | }); | 775 | }); |
| 775 | setComposerHighlightedItemId(null); | 776 | setComposerHighlightedItemId(null); |
| 776 | }, [canCheckoutPullRequestIntoThread]); | 777 | }, [canCheckoutPullRequestIntoThread]); |
| 777 | 778 | ||
| 778 | const closePullRequestComposerMenu = useCallback(() => { | 779 | const closePullRequestDialog = useCallback(() => { |
| 779 | setPullRequestComposerMenuState(null); | 780 | setPullRequestDialogState(null); |
| 780 | }, []); | 781 | }, []); |
| 781 | 782 | ||
| 782 | const openOrReuseProjectDraftThread = useCallback( | 783 | const openOrReuseProjectDraftThread = useCallback( |
| ... | @@ -1254,7 +1255,6 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -1254,7 +1255,6 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1254 | timelineEntries, | 1255 | timelineEntries, |
| 1255 | ]); | 1256 | ]); |
| 1256 | const gitCwd = activeThread?.worktreePath ?? activeProject?.cwd ?? null; | 1257 | const gitCwd = activeThread?.worktreePath ?? activeProject?.cwd ?? null; |
| 1257 | const pullRequestComposerMenuOpen = pullRequestComposerMenuState !== null; | ||
| 1258 | const composerTriggerKind = composerTrigger?.kind ?? null; | 1258 | const composerTriggerKind = composerTrigger?.kind ?? null; |
| 1259 | const pathTriggerQuery = composerTrigger?.kind === "path" ? composerTrigger.query : ""; | 1259 | const pathTriggerQuery = composerTrigger?.kind === "path" ? composerTrigger.query : ""; |
| 1260 | const isPathTrigger = composerTriggerKind === "path"; | 1260 | const isPathTrigger = composerTriggerKind === "path"; |
| ... | @@ -1311,22 +1311,12 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -1311,22 +1311,12 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1311 | label: "/default", | 1311 | label: "/default", |
| 1312 | description: "Switch this thread back to normal chat mode", | 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 | ] satisfies ReadonlyArray<Extract<ComposerCommandItem, { type: "slash-command" }>>; | 1314 | ] satisfies ReadonlyArray<Extract<ComposerCommandItem, { type: "slash-command" }>>; |
| 1322 | const availableSlashCommandItems = canCheckoutPullRequestIntoThread | ||
| 1323 | ? slashCommandItems | ||
| 1324 | : slashCommandItems.filter((item) => item.command !== "checkout-pr"); | ||
| 1325 | const query = composerTrigger.query.trim().toLowerCase(); | 1315 | const query = composerTrigger.query.trim().toLowerCase(); |
| 1326 | if (!query) { | 1316 | if (!query) { |
| 1327 | return [...availableSlashCommandItems]; | 1317 | return [...slashCommandItems]; |
| 1328 | } | 1318 | } |
| 1329 | return availableSlashCommandItems.filter( | 1319 | return slashCommandItems.filter( |
| 1330 | (item) => item.command.includes(query) || item.label.slice(1).includes(query), | 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) { | ... | @@ -1347,16 +1337,14 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1347 | label: name, | 1337 | label: name, |
| 1348 | description: `${providerLabel} · ${slug}`, | 1338 | description: `${providerLabel} · ${slug}`, |
| 1349 | })); | 1339 | })); |
| 1350 | }, [canCheckoutPullRequestIntoThread, composerTrigger, searchableModelOptions, workspaceEntries]); | 1340 | }, [composerTrigger, searchableModelOptions, workspaceEntries]); |
| 1351 | const composerMenuOpen = Boolean(composerTrigger) || pullRequestComposerMenuOpen; | 1341 | const composerMenuOpen = Boolean(composerTrigger); |
| 1352 | const activeComposerMenuItem = useMemo( | 1342 | const activeComposerMenuItem = useMemo( |
| 1353 | () => | 1343 | () => |
| 1354 | pullRequestComposerMenuOpen | 1344 | composerMenuItems.find((item) => item.id === composerHighlightedItemId) ?? |
| 1355 | ? null | 1345 | composerMenuItems[0] ?? |
| 1356 | : (composerMenuItems.find((item) => item.id === composerHighlightedItemId) ?? | 1346 | null, |
| 1357 | composerMenuItems[0] ?? | 1347 | [composerHighlightedItemId, composerMenuItems], |
| 1358 | null), | ||
| 1359 | [composerHighlightedItemId, composerMenuItems, pullRequestComposerMenuOpen], | ||
| 1360 | ); | 1348 | ); |
| 1361 | composerMenuOpenRef.current = composerMenuOpen; | 1349 | composerMenuOpenRef.current = composerMenuOpen; |
| 1362 | composerMenuItemsRef.current = composerMenuItems; | 1350 | composerMenuItemsRef.current = composerMenuItems; |
| ... | @@ -2055,7 +2043,7 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -2055,7 +2043,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2055 | 2043 | ||
| 2056 | useEffect(() => { | 2044 | useEffect(() => { |
| 2057 | setExpandedWorkGroups({}); | 2045 | setExpandedWorkGroups({}); |
| 2058 | setPullRequestComposerMenuState(null); | 2046 | setPullRequestDialogState(null); |
| 2059 | if (planSidebarOpenOnNextThreadRef.current) { | 2047 | if (planSidebarOpenOnNextThreadRef.current) { |
| 2060 | planSidebarOpenOnNextThreadRef.current = false; | 2048 | planSidebarOpenOnNextThreadRef.current = false; |
| 2061 | setPlanSidebarOpen(true); | 2049 | setPlanSidebarOpen(true); |
| ... | @@ -2070,16 +2058,12 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -2070,16 +2058,12 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2070 | setComposerHighlightedItemId(null); | 2058 | setComposerHighlightedItemId(null); |
| 2071 | return; | 2059 | return; |
| 2072 | } | 2060 | } |
| 2073 | if (pullRequestComposerMenuOpen) { | ||
| 2074 | setComposerHighlightedItemId(null); | ||
| 2075 | return; | ||
| 2076 | } | ||
| 2077 | setComposerHighlightedItemId((existing) => | 2061 | setComposerHighlightedItemId((existing) => |
| 2078 | existing && composerMenuItems.some((item) => item.id === existing) | 2062 | existing && composerMenuItems.some((item) => item.id === existing) |
| 2079 | ? existing | 2063 | ? existing |
| 2080 | : (composerMenuItems[0]?.id ?? null), | 2064 | : (composerMenuItems[0]?.id ?? null), |
| 2081 | ); | 2065 | ); |
| 2082 | }, [composerMenuItems, composerMenuOpen, pullRequestComposerMenuOpen]); | 2066 | }, [composerMenuItems, composerMenuOpen]); |
| 2083 | 2067 | ||
| 2084 | useEffect(() => { | 2068 | useEffect(() => { |
| 2085 | setIsRevertingCheckpoint(false); | 2069 | setIsRevertingCheckpoint(false); |
| ... | @@ -2589,15 +2573,8 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -2589,15 +2573,8 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2589 | } | 2573 | } |
| 2590 | const standaloneSlashCommand = | 2574 | const standaloneSlashCommand = |
| 2591 | composerImages.length === 0 ? parseStandaloneComposerSlashCommand(trimmed) : null; | 2575 | composerImages.length === 0 ? parseStandaloneComposerSlashCommand(trimmed) : null; |
| 2592 | const shouldHandleStandaloneSlashCommand = | 2576 | if (standaloneSlashCommand) { |
| 2593 | standaloneSlashCommand !== null && | 2577 | await handleInteractionModeChange(standaloneSlashCommand); |
| 2594 | (standaloneSlashCommand !== "checkout-pr" || canCheckoutPullRequestIntoThread); | ||
| 2595 | if (shouldHandleStandaloneSlashCommand && standaloneSlashCommand) { | ||
| 2596 | if (standaloneSlashCommand === "checkout-pr") { | ||
| 2597 | openPullRequestComposerMenu(); | ||
| 2598 | } else { | ||
| 2599 | await handleInteractionModeChange(standaloneSlashCommand); | ||
| 2600 | } | ||
| 2601 | promptRef.current = ""; | 2578 | promptRef.current = ""; |
| 2602 | clearComposerDraftContent(activeThread.id); | 2579 | clearComposerDraftContent(activeThread.id); |
| 2603 | setComposerHighlightedItemId(null); | 2580 | setComposerHighlightedItemId(null); |
| ... | @@ -3386,11 +3363,7 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -3386,11 +3363,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 3386 | } | 3363 | } |
| 3387 | return; | 3364 | return; |
| 3388 | } | 3365 | } |
| 3389 | if (item.command === "checkout-pr") { | 3366 | void handleInteractionModeChange(item.command === "plan" ? "plan" : "default"); |
| 3390 | openPullRequestComposerMenu(); | ||
| 3391 | } else { | ||
| 3392 | void handleInteractionModeChange(item.command === "plan" ? "plan" : "default"); | ||
| 3393 | } | ||
| 3394 | const applied = applyPromptReplacement(trigger.rangeStart, trigger.rangeEnd, "", { | 3367 | const applied = applyPromptReplacement(trigger.rangeStart, trigger.rangeEnd, "", { |
| 3395 | expectedText: expectedToken, | 3368 | expectedText: expectedToken, |
| 3396 | }); | 3369 | }); |
| ... | @@ -3410,7 +3383,6 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -3410,7 +3383,6 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 3410 | [ | 3383 | [ |
| 3411 | applyPromptReplacement, | 3384 | applyPromptReplacement, |
| 3412 | handleInteractionModeChange, | 3385 | handleInteractionModeChange, |
| 3413 | openPullRequestComposerMenu, | ||
| 3414 | onProviderModelSelect, | 3386 | onProviderModelSelect, |
| 3415 | resolveActiveComposerTrigger, | 3387 | resolveActiveComposerTrigger, |
| 3416 | ], | 3388 | ], |
| ... | @@ -3485,12 +3457,6 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -3485,12 +3457,6 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 3485 | const { trigger } = resolveActiveComposerTrigger(); | 3457 | const { trigger } = resolveActiveComposerTrigger(); |
| 3486 | const menuIsActive = composerMenuOpenRef.current || trigger !== null; | 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 | if (menuIsActive) { | 3460 | if (menuIsActive) { |
| 3495 | const currentItems = composerMenuItemsRef.current; | 3461 | const currentItems = composerMenuItemsRef.current; |
| 3496 | if (key === "ArrowDown" && currentItems.length > 0) { | 3462 | if (key === "ArrowDown" && currentItems.length > 0) { |
| ... | @@ -3712,28 +3678,15 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -3712,28 +3678,15 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 3712 | > | 3678 | > |
| 3713 | {composerMenuOpen && !isComposerApprovalState && ( | 3679 | {composerMenuOpen && !isComposerApprovalState && ( |
| 3714 | <div className="absolute inset-x-0 bottom-full z-20 mb-2 px-1"> | 3680 | <div className="absolute inset-x-0 bottom-full z-20 mb-2 px-1"> |
| 3715 | {pullRequestComposerMenuState ? ( | 3681 | <ComposerCommandMenu |
| 3716 | <ComposerPullRequestCommandMenu | 3682 | items={composerMenuItems} |
| 3717 | key={pullRequestComposerMenuState.key} | 3683 | resolvedTheme={resolvedTheme} |
| 3718 | cwd={activeProject?.cwd ?? null} | 3684 | isLoading={isComposerMenuLoading} |
| 3719 | initialReference={pullRequestComposerMenuState.initialReference} | 3685 | triggerKind={composerTriggerKind} |
| 3720 | onCancel={closePullRequestComposerMenu} | 3686 | activeItemId={activeComposerMenuItem?.id ?? null} |
| 3721 | onPrepared={async (input) => { | 3687 | onHighlightedItemChange={onComposerMenuItemHighlighted} |
| 3722 | await handlePreparedPullRequestThread(input); | 3688 | onSelect={onSelectComposerItem} |
| 3723 | closePullRequestComposerMenu(); | 3689 | /> |
| 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 | )} | ||
| 3737 | </div> | 3690 | </div> |
| 3738 | )} | 3691 | )} |
| 3739 | 3692 | ||
| ... | @@ -4160,10 +4113,24 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -4160,10 +4113,24 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 4160 | envLocked={envLocked} | 4113 | envLocked={envLocked} |
| 4161 | onComposerFocusRequest={scheduleComposerFocus} | 4114 | onComposerFocusRequest={scheduleComposerFocus} |
| 4162 | {...(canCheckoutPullRequestIntoThread | 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 | </div>{/* end chat column */} | 4134 | </div>{/* end chat column */} |
| 4168 | 4135 | ||
| 4169 | {/* Plan sidebar */} | 4136 | {/* Plan sidebar */} |
apps/web/src/components/ComposerPullRequestCommandMenu.tsx deleted-259| ... | @@ -1,259 +0,0 @@ | ||
| 1 | import type { GitResolvePullRequestResult } from "@t3tools/contracts"; | ||
| 2 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | ||
| 3 | import { useDebouncedValue } from "@tanstack/react-pacer"; | ||
| 4 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | ||
| 5 | |||
| 6 | import { | ||
| 7 | gitPreparePullRequestThreadMutationOptions, | ||
| 8 | gitResolvePullRequestQueryOptions, | ||
| 9 | } from "~/lib/gitReactQuery"; | ||
| 10 | import { cn } from "~/lib/utils"; | ||
| 11 | import { parsePullRequestReference } from "~/pullRequestReference"; | ||
| 12 | import { Button } from "./ui/button"; | ||
| 13 | import { Input } from "./ui/input"; | ||
| 14 | import { Spinner } from "./ui/spinner"; | ||
| 15 | |||
| 16 | interface ComposerPullRequestCommandMenuProps { | ||
| 17 | cwd: string | null; | ||
| 18 | initialReference: string | null; | ||
| 19 | onCancel: () => void; | ||
| 20 | onPrepared: (input: { branch: string; worktreePath: string | null }) => Promise<void> | void; | ||
| 21 | } | ||
| 22 | |||
| 23 | export function ComposerPullRequestCommandMenu({ | ||
| 24 | cwd, | ||
| 25 | initialReference, | ||
| 26 | onCancel, | ||
| 27 | onPrepared, | ||
| 28 | }: ComposerPullRequestCommandMenuProps) { | ||
| 29 | const queryClient = useQueryClient(); | ||
| 30 | const referenceInputRef = useRef<HTMLInputElement>(null); | ||
| 31 | const [reference, setReference] = useState(initialReference ?? ""); | ||
| 32 | const [referenceDirty, setReferenceDirty] = useState(false); | ||
| 33 | const [preparingMode, setPreparingMode] = useState<"local" | "worktree" | null>(null); | ||
| 34 | const [debouncedReference, referenceDebouncer] = useDebouncedValue( | ||
| 35 | reference, | ||
| 36 | { wait: 450 }, | ||
| 37 | (debouncerState) => ({ isPending: debouncerState.isPending }), | ||
| 38 | ); | ||
| 39 | |||
| 40 | useEffect(() => { | ||
| 41 | setReference(initialReference ?? ""); | ||
| 42 | setReferenceDirty(false); | ||
| 43 | setPreparingMode(null); | ||
| 44 | }, [initialReference]); | ||
| 45 | |||
| 46 | useEffect(() => { | ||
| 47 | const frame = window.requestAnimationFrame(() => { | ||
| 48 | referenceInputRef.current?.focus(); | ||
| 49 | referenceInputRef.current?.select(); | ||
| 50 | }); | ||
| 51 | return () => { | ||
| 52 | window.cancelAnimationFrame(frame); | ||
| 53 | }; | ||
| 54 | }, []); | ||
| 55 | |||
| 56 | const parsedReference = parsePullRequestReference(reference); | ||
| 57 | const parsedDebouncedReference = parsePullRequestReference(debouncedReference); | ||
| 58 | const resolvePullRequestQuery = useQuery( | ||
| 59 | gitResolvePullRequestQueryOptions({ | ||
| 60 | cwd, | ||
| 61 | reference: parsedDebouncedReference, | ||
| 62 | }), | ||
| 63 | ); | ||
| 64 | const cachedPullRequest = useMemo(() => { | ||
| 65 | if (!cwd || !parsedReference) { | ||
| 66 | return null; | ||
| 67 | } | ||
| 68 | const cached = queryClient.getQueryData<GitResolvePullRequestResult>([ | ||
| 69 | "git", | ||
| 70 | "pull-request", | ||
| 71 | cwd, | ||
| 72 | parsedReference, | ||
| 73 | ]); | ||
| 74 | return cached?.pullRequest ?? null; | ||
| 75 | }, [cwd, parsedReference, queryClient]); | ||
| 76 | const preparePullRequestThreadMutation = useMutation( | ||
| 77 | gitPreparePullRequestThreadMutationOptions({ cwd, queryClient }), | ||
| 78 | ); | ||
| 79 | |||
| 80 | const liveResolvedPullRequest = | ||
| 81 | parsedReference !== null && parsedReference === parsedDebouncedReference | ||
| 82 | ? (resolvePullRequestQuery.data?.pullRequest ?? null) | ||
| 83 | : null; | ||
| 84 | const resolvedPullRequest = liveResolvedPullRequest ?? cachedPullRequest; | ||
| 85 | const isResolving = | ||
| 86 | parsedReference !== null && | ||
| 87 | resolvedPullRequest === null && | ||
| 88 | (referenceDebouncer.state.isPending || | ||
| 89 | parsedReference !== parsedDebouncedReference || | ||
| 90 | resolvePullRequestQuery.isPending || | ||
| 91 | resolvePullRequestQuery.isFetching); | ||
| 92 | const statusTone = useMemo(() => { | ||
| 93 | switch (resolvedPullRequest?.state) { | ||
| 94 | case "merged": | ||
| 95 | return "text-violet-600 dark:text-violet-300/90"; | ||
| 96 | case "closed": | ||
| 97 | return "text-zinc-500 dark:text-zinc-400/80"; | ||
| 98 | case "open": | ||
| 99 | return "text-emerald-600 dark:text-emerald-300/90"; | ||
| 100 | default: | ||
| 101 | return "text-muted-foreground"; | ||
| 102 | } | ||
| 103 | }, [resolvedPullRequest?.state]); | ||
| 104 | |||
| 105 | const handleConfirm = useCallback(async (mode: "local" | "worktree") => { | ||
| 106 | if (!parsedReference) { | ||
| 107 | setReferenceDirty(true); | ||
| 108 | return; | ||
| 109 | } | ||
| 110 | if (!parsedReference || !resolvedPullRequest || !cwd) { | ||
| 111 | return; | ||
| 112 | } | ||
| 113 | setPreparingMode(mode); | ||
| 114 | try { | ||
| 115 | const result = await preparePullRequestThreadMutation.mutateAsync({ | ||
| 116 | reference: parsedReference, | ||
| 117 | mode, | ||
| 118 | }); | ||
| 119 | await onPrepared({ | ||
| 120 | branch: result.branch, | ||
| 121 | worktreePath: result.worktreePath, | ||
| 122 | }); | ||
| 123 | } finally { | ||
| 124 | setPreparingMode(null); | ||
| 125 | } | ||
| 126 | }, [ | ||
| 127 | cwd, | ||
| 128 | onPrepared, | ||
| 129 | parsedReference, | ||
| 130 | preparePullRequestThreadMutation, | ||
| 131 | resolvedPullRequest, | ||
| 132 | ]); | ||
| 133 | |||
| 134 | const validationMessage = | ||
| 135 | !referenceDirty | ||
| 136 | ? null | ||
| 137 | : 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; | ||
| 142 | const errorMessage = | ||
| 143 | validationMessage ?? | ||
| 144 | (resolvedPullRequest === null && resolvePullRequestQuery.isError | ||
| 145 | ? (resolvePullRequestQuery.error instanceof Error | ||
| 146 | ? resolvePullRequestQuery.error.message | ||
| 147 | : "Failed to resolve pull request.") | ||
| 148 | : (preparePullRequestThreadMutation.error instanceof Error | ||
| 149 | ? preparePullRequestThreadMutation.error.message | ||
| 150 | : preparePullRequestThreadMutation.error | ||
| 151 | ? "Failed to prepare pull request thread." | ||
| 152 | : null)); | ||
| 153 | |||
| 154 | 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> | ||
| 186 | |||
| 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> | ||
| 196 | </div> | ||
| 197 | <span className={cn("shrink-0 text-xs capitalize", statusTone)}> | ||
| 198 | {resolvedPullRequest.state} | ||
| 199 | </span> | ||
| 200 | </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} | ||
| 210 | |||
| 211 | {errorMessage ? <p className="text-destructive text-xs">{errorMessage}</p> : null} | ||
| 212 | </div> | ||
| 213 | |||
| 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}> | ||
| 222 | Cancel | ||
| 223 | </Button> | ||
| 224 | <Button | ||
| 225 | type="button" | ||
| 226 | size="sm" | ||
| 227 | variant="outline" | ||
| 228 | onClick={() => { | ||
| 229 | void handleConfirm("local"); | ||
| 230 | }} | ||
| 231 | disabled={ | ||
| 232 | !cwd || | ||
| 233 | !resolvedPullRequest || | ||
| 234 | isResolving || | ||
| 235 | preparePullRequestThreadMutation.isPending | ||
| 236 | } | ||
| 237 | > | ||
| 238 | {preparingMode === "local" ? "Preparing local..." : "Local"} | ||
| 239 | </Button> | ||
| 240 | <Button | ||
| 241 | type="button" | ||
| 242 | size="sm" | ||
| 243 | onClick={() => { | ||
| 244 | void handleConfirm("worktree"); | ||
| 245 | }} | ||
| 246 | disabled={ | ||
| 247 | !cwd || | ||
| 248 | !resolvedPullRequest || | ||
| 249 | isResolving || | ||
| 250 | preparePullRequestThreadMutation.isPending | ||
| 251 | } | ||
| 252 | > | ||
| 253 | {preparingMode === "worktree" ? "Preparing worktree..." : "Worktree"} | ||
| 254 | </Button> | ||
| 255 | </div> | ||
| 256 | </div> | ||
| 257 | </div> | ||
| 258 | ); | ||
| 259 | } | ||
apps/web/src/components/PullRequestThreadDialog.tsx created+281| ... | @@ -0,0 +1,281 @@ | ||
| 1 | import type { GitResolvePullRequestResult } from "@t3tools/contracts"; | ||
| 2 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | ||
| 3 | import { useDebouncedValue } from "@tanstack/react-pacer"; | ||
| 4 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | ||
| 5 | |||
| 6 | import { | ||
| 7 | gitPreparePullRequestThreadMutationOptions, | ||
| 8 | gitResolvePullRequestQueryOptions, | ||
| 9 | } from "~/lib/gitReactQuery"; | ||
| 10 | import { cn } from "~/lib/utils"; | ||
| 11 | import { parsePullRequestReference } from "~/pullRequestReference"; | ||
| 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"; | ||
| 22 | import { Input } from "./ui/input"; | ||
| 23 | import { Spinner } from "./ui/spinner"; | ||
| 24 | |||
| 25 | interface PullRequestThreadDialogProps { | ||
| 26 | open: boolean; | ||
| 27 | cwd: string | null; | ||
| 28 | initialReference: string | null; | ||
| 29 | onOpenChange: (open: boolean) => void; | ||
| 30 | onPrepared: (input: { branch: string; worktreePath: string | null }) => Promise<void> | void; | ||
| 31 | } | ||
| 32 | |||
| 33 | export function PullRequestThreadDialog({ | ||
| 34 | open, | ||
| 35 | cwd, | ||
| 36 | initialReference, | ||
| 37 | onOpenChange, | ||
| 38 | onPrepared, | ||
| 39 | }: PullRequestThreadDialogProps) { | ||
| 40 | const queryClient = useQueryClient(); | ||
| 41 | const referenceInputRef = useRef<HTMLInputElement>(null); | ||
| 42 | const [reference, setReference] = useState(initialReference ?? ""); | ||
| 43 | const [referenceDirty, setReferenceDirty] = useState(false); | ||
| 44 | const [preparingMode, setPreparingMode] = useState<"local" | "worktree" | null>(null); | ||
| 45 | const [debouncedReference, referenceDebouncer] = useDebouncedValue( | ||
| 46 | reference, | ||
| 47 | { wait: 450 }, | ||
| 48 | (debouncerState) => ({ isPending: debouncerState.isPending }), | ||
| 49 | ); | ||
| 50 | |||
| 51 | useEffect(() => { | ||
| 52 | if (!open) return; | ||
| 53 | setReference(initialReference ?? ""); | ||
| 54 | setReferenceDirty(false); | ||
| 55 | setPreparingMode(null); | ||
| 56 | }, [initialReference, open]); | ||
| 57 | |||
| 58 | useEffect(() => { | ||
| 59 | if (!open) return; | ||
| 60 | const frame = window.requestAnimationFrame(() => { | ||
| 61 | referenceInputRef.current?.focus(); | ||
| 62 | referenceInputRef.current?.select(); | ||
| 63 | }); | ||
| 64 | return () => { | ||
| 65 | window.cancelAnimationFrame(frame); | ||
| 66 | }; | ||
| 67 | }, [open]); | ||
| 68 | |||
| 69 | const parsedReference = parsePullRequestReference(reference); | ||
| 70 | const parsedDebouncedReference = parsePullRequestReference(debouncedReference); | ||
| 71 | const resolvePullRequestQuery = useQuery( | ||
| 72 | gitResolvePullRequestQueryOptions({ | ||
| 73 | cwd, | ||
| 74 | reference: open ? parsedDebouncedReference : null, | ||
| 75 | }), | ||
| 76 | ); | ||
| 77 | const cachedPullRequest = useMemo(() => { | ||
| 78 | if (!cwd || !parsedReference) { | ||
| 79 | return null; | ||
| 80 | } | ||
| 81 | const cached = queryClient.getQueryData<GitResolvePullRequestResult>([ | ||
| 82 | "git", | ||
| 83 | "pull-request", | ||
| 84 | cwd, | ||
| 85 | parsedReference, | ||
| 86 | ]); | ||
| 87 | return cached?.pullRequest ?? null; | ||
| 88 | }, [cwd, parsedReference, queryClient]); | ||
| 89 | const preparePullRequestThreadMutation = useMutation( | ||
| 90 | gitPreparePullRequestThreadMutationOptions({ cwd, queryClient }), | ||
| 91 | ); | ||
| 92 | |||
| 93 | const liveResolvedPullRequest = | ||
| 94 | parsedReference !== null && parsedReference === parsedDebouncedReference | ||
| 95 | ? (resolvePullRequestQuery.data?.pullRequest ?? null) | ||
| 96 | : null; | ||
| 97 | const resolvedPullRequest = liveResolvedPullRequest ?? cachedPullRequest; | ||
| 98 | const isResolving = | ||
| 99 | open && | ||
| 100 | parsedReference !== null && | ||
| 101 | resolvedPullRequest === null && | ||
| 102 | (referenceDebouncer.state.isPending || | ||
| 103 | parsedReference !== parsedDebouncedReference || | ||
| 104 | resolvePullRequestQuery.isPending || | ||
| 105 | resolvePullRequestQuery.isFetching); | ||
| 106 | const statusTone = useMemo(() => { | ||
| 107 | switch (resolvedPullRequest?.state) { | ||
| 108 | case "merged": | ||
| 109 | return "text-violet-600 dark:text-violet-300/90"; | ||
| 110 | case "closed": | ||
| 111 | return "text-zinc-500 dark:text-zinc-400/80"; | ||
| 112 | case "open": | ||
| 113 | return "text-emerald-600 dark:text-emerald-300/90"; | ||
| 114 | default: | ||
| 115 | return "text-muted-foreground"; | ||
| 116 | } | ||
| 117 | }, [resolvedPullRequest?.state]); | ||
| 118 | |||
| 119 | const handleConfirm = useCallback(async (mode: "local" | "worktree") => { | ||
| 120 | if (!parsedReference) { | ||
| 121 | setReferenceDirty(true); | ||
| 122 | return; | ||
| 123 | } | ||
| 124 | if (!parsedReference || !resolvedPullRequest || !cwd) { | ||
| 125 | return; | ||
| 126 | } | ||
| 127 | setPreparingMode(mode); | ||
| 128 | try { | ||
| 129 | const result = await preparePullRequestThreadMutation.mutateAsync({ | ||
| 130 | reference: parsedReference, | ||
| 131 | mode, | ||
| 132 | }); | ||
| 133 | await onPrepared({ | ||
| 134 | branch: result.branch, | ||
| 135 | worktreePath: result.worktreePath, | ||
| 136 | }); | ||
| 137 | onOpenChange(false); | ||
| 138 | } finally { | ||
| 139 | setPreparingMode(null); | ||
| 140 | } | ||
| 141 | }, [ | ||
| 142 | cwd, | ||
| 143 | onOpenChange, | ||
| 144 | onPrepared, | ||
| 145 | parsedReference, | ||
| 146 | preparePullRequestThreadMutation, | ||
| 147 | resolvedPullRequest, | ||
| 148 | ]); | ||
| 149 | |||
| 150 | const validationMessage = | ||
| 151 | !referenceDirty | ||
| 152 | ? null | ||
| 153 | : reference.trim().length === 0 | ||
| 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; | ||
| 158 | const errorMessage = | ||
| 159 | validationMessage ?? | ||
| 160 | (resolvedPullRequest === null && resolvePullRequestQuery.isError | ||
| 161 | ? (resolvePullRequestQuery.error instanceof Error | ||
| 162 | ? resolvePullRequestQuery.error.message | ||
| 163 | : "Failed to resolve pull request.") | ||
| 164 | : (preparePullRequestThreadMutation.error instanceof Error | ||
| 165 | ? preparePullRequestThreadMutation.error.message | ||
| 166 | : preparePullRequestThreadMutation.error | ||
| 167 | ? "Failed to prepare pull request thread." | ||
| 168 | : null)); | ||
| 169 | |||
| 170 | return ( | ||
| 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> | ||
| 209 | |||
| 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> | ||
| 223 | </div> | ||
| 224 | </div> | ||
| 225 | ) : null} | ||
| 226 | |||
| 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} | ||
| 233 | |||
| 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 | > | ||
| 244 | Cancel | ||
| 245 | </Button> | ||
| 246 | <Button | ||
| 247 | type="button" | ||
| 248 | size="sm" | ||
| 249 | variant="outline" | ||
| 250 | onClick={() => { | ||
| 251 | void handleConfirm("local"); | ||
| 252 | }} | ||
| 253 | disabled={ | ||
| 254 | !cwd || | ||
| 255 | !resolvedPullRequest || | ||
| 256 | isResolving || | ||
| 257 | preparePullRequestThreadMutation.isPending | ||
| 258 | } | ||
| 259 | > | ||
| 260 | {preparingMode === "local" ? "Preparing local..." : "Local"} | ||
| 261 | </Button> | ||
| 262 | <Button | ||
| 263 | type="button" | ||
| 264 | size="sm" | ||
| 265 | onClick={() => { | ||
| 266 | void handleConfirm("worktree"); | ||
| 267 | }} | ||
| 268 | disabled={ | ||
| 269 | !cwd || | ||
| 270 | !resolvedPullRequest || | ||
| 271 | isResolving || | ||
| 272 | preparePullRequestThreadMutation.isPending | ||
| 273 | } | ||
| 274 | > | ||
| 275 | {preparingMode === "worktree" ? "Preparing worktree..." : "Worktree"} | ||
| 276 | </Button> | ||
| 277 | </DialogFooter> | ||
| 278 | </DialogPopup> | ||
| 279 | </Dialog> | ||
| 280 | ); | ||
| 281 | } | ||
apps/web/src/composer-logic.test.ts+4-4| ... | @@ -134,11 +134,11 @@ describe("parseStandaloneComposerSlashCommand", () => { | ... | @@ -134,11 +134,11 @@ describe("parseStandaloneComposerSlashCommand", () => { |
| 134 | expect(parseStandaloneComposerSlashCommand("/default")).toBe("default"); | 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 | it("ignores slash commands with extra message text", () => { | 137 | it("ignores slash commands with extra message text", () => { |
| 142 | expect(parseStandaloneComposerSlashCommand("/plan explain this")).toBeNull(); | 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 | }); |
apps/web/src/composer-logic.ts+2-4| ... | @@ -1,7 +1,7 @@ | ... | @@ -1,7 +1,7 @@ |
| 1 | import { splitPromptIntoComposerSegments } from "./composer-editor-mentions"; | 1 | import { splitPromptIntoComposerSegments } from "./composer-editor-mentions"; |
| 2 | 2 | ||
| 3 | export type ComposerTriggerKind = "path" | "slash-command" | "slash-model"; | 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 | export interface ComposerTrigger { | 6 | export interface ComposerTrigger { |
| 7 | kind: ComposerTriggerKind; | 7 | kind: ComposerTriggerKind; |
| ... | @@ -14,7 +14,6 @@ const SLASH_COMMANDS: readonly ComposerSlashCommand[] = [ | ... | @@ -14,7 +14,6 @@ const SLASH_COMMANDS: readonly ComposerSlashCommand[] = [ |
| 14 | "model", | 14 | "model", |
| 15 | "plan", | 15 | "plan", |
| 16 | "default", | 16 | "default", |
| 17 | "checkout-pr", | ||
| 18 | ]; | 17 | ]; |
| 19 | 18 | ||
| 20 | function clampCursor(text: string, cursor: number): number { | 19 | function clampCursor(text: string, cursor: number): number { |
| ... | @@ -169,13 +168,12 @@ export function parseStandaloneComposerSlashCommand(text: string): Exclude< | ... | @@ -169,13 +168,12 @@ export function parseStandaloneComposerSlashCommand(text: string): Exclude< |
| 169 | ComposerSlashCommand, | 168 | ComposerSlashCommand, |
| 170 | "model" | 169 | "model" |
| 171 | > | null { | 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 | if (!match) { | 172 | if (!match) { |
| 174 | return null; | 173 | return null; |
| 175 | } | 174 | } |
| 176 | const command = match[1]?.toLowerCase(); | 175 | const command = match[1]?.toLowerCase(); |
| 177 | if (command === "plan") return "plan"; | 176 | if (command === "plan") return "plan"; |
| 178 | if (command === "checkout-pr") return "checkout-pr"; | ||
| 179 | return "default"; | 177 | return "default"; |
| 180 | } | 178 | } |
| 181 | 179 |