diff --git a/apps/web/src/components/BranchToolbarBranchSelector.tsx b/apps/web/src/components/BranchToolbarBranchSelector.tsx index 1817e29077cdf4bc98a5fd2058c6401deb39d6a1..dbef056ca99dce203fea8358eb06dfc2f33bf7ce 100644 --- a/apps/web/src/components/BranchToolbarBranchSelector.tsx +++ b/apps/web/src/components/BranchToolbarBranchSelector.tsx @@ -291,7 +291,8 @@ export function BranchToolbarBranchSelector({ const branchListScrollElementRef = useRef(null); const branchListVirtualizer = useVirtualizer({ count: filteredBranchPickerItems.length, - estimateSize: () => 28, + estimateSize: (index) => + filteredBranchPickerItems[index] === checkoutPullRequestItemValue ? 44 : 28, getScrollElement: () => branchListScrollElementRef.current, overscan: 12, enabled: isBranchMenuOpen && shouldVirtualizeBranchList, @@ -409,7 +410,7 @@ export function BranchToolbarBranchSelector({ items={branchPickerItems} filteredItems={filteredBranchPickerItems} autoHighlight - virtualized + virtualized={shouldVirtualizeBranchList} onItemHighlighted={(_value, eventDetails) => { if (!isBranchMenuOpen || eventDetails.index < 0) return; branchListVirtualizer.scrollToIndex(eventDetails.index, { align: "auto" }); diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 80d440994458d0d9e1d46dc45b6dd7b2f1ff3bcd..ee3de304eae168c41a0e098fc7e46bb2d72610ac 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -119,7 +119,6 @@ import { type TurnDiffTreeNode, } from "../lib/turnDiffTree"; import BranchToolbar from "./BranchToolbar"; -import { ComposerPullRequestCommandMenu } from "./ComposerPullRequestCommandMenu"; import GitActionsControl from "./GitActionsControl"; import { isOpenFavoriteEditorShortcut, @@ -217,6 +216,7 @@ import { shouldUseCompactComposerFooter } from "./composerFooterLayout"; import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; import { clamp } from "effect/Number"; import { ComposerPromptEditor, type ComposerPromptEditorHandle } from "./ComposerPromptEditor"; +import { PullRequestThreadDialog } from "./PullRequestThreadDialog"; import { estimateTimelineMessageHeight } from "./timelineHeight"; function formatMessageMeta(createdAt: string, duration: string | null): string { @@ -408,7 +408,7 @@ type ComposerCommandItem = type SendPhase = "idle" | "preparing-worktree" | "sending-turn"; -interface PullRequestComposerMenuState { +interface PullRequestDialogState { initialReference: string | null; key: number; } @@ -657,8 +657,9 @@ export default function ChatView({ threadId }: ChatViewProps) { const [nowTick, setNowTick] = useState(() => Date.now()); const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0); const [composerHighlightedItemId, setComposerHighlightedItemId] = useState(null); - const [pullRequestComposerMenuState, setPullRequestComposerMenuState] = - useState(null); + const [pullRequestDialogState, setPullRequestDialogState] = useState( + null, + ); const [attachmentPreviewHandoffByMessageId, setAttachmentPreviewHandoffByMessageId] = useState< Record >({}); @@ -764,19 +765,19 @@ export default function ChatView({ threadId }: ChatViewProps) { const latestTurnSettled = isLatestTurnSettled(activeLatestTurn, activeThread?.session ?? null); const activeProject = projects.find((p) => p.id === activeThread?.projectId); - const openPullRequestComposerMenu = useCallback((reference?: string) => { + const openPullRequestDialog = useCallback((reference?: string) => { if (!canCheckoutPullRequestIntoThread) { return; } - setPullRequestComposerMenuState({ + setPullRequestDialogState({ initialReference: reference ?? null, key: Date.now(), }); setComposerHighlightedItemId(null); }, [canCheckoutPullRequestIntoThread]); - const closePullRequestComposerMenu = useCallback(() => { - setPullRequestComposerMenuState(null); + const closePullRequestDialog = useCallback(() => { + setPullRequestDialogState(null); }, []); const openOrReuseProjectDraftThread = useCallback( @@ -1254,7 +1255,6 @@ export default function ChatView({ threadId }: ChatViewProps) { timelineEntries, ]); const gitCwd = activeThread?.worktreePath ?? activeProject?.cwd ?? null; - const pullRequestComposerMenuOpen = pullRequestComposerMenuState !== null; const composerTriggerKind = composerTrigger?.kind ?? null; const pathTriggerQuery = composerTrigger?.kind === "path" ? composerTrigger.query : ""; const isPathTrigger = composerTriggerKind === "path"; @@ -1311,22 +1311,12 @@ export default function ChatView({ threadId }: ChatViewProps) { label: "/default", description: "Switch this thread back to normal chat mode", }, - { - id: "slash:checkout-pr", - type: "slash-command", - command: "checkout-pr", - label: "/checkout-pr", - description: "Create a new thread from a GitHub pull request", - }, ] satisfies ReadonlyArray>; - const availableSlashCommandItems = canCheckoutPullRequestIntoThread - ? slashCommandItems - : slashCommandItems.filter((item) => item.command !== "checkout-pr"); const query = composerTrigger.query.trim().toLowerCase(); if (!query) { - return [...availableSlashCommandItems]; + return [...slashCommandItems]; } - return availableSlashCommandItems.filter( + return slashCommandItems.filter( (item) => item.command.includes(query) || item.label.slice(1).includes(query), ); } @@ -1347,16 +1337,14 @@ export default function ChatView({ threadId }: ChatViewProps) { label: name, description: `${providerLabel} · ${slug}`, })); - }, [canCheckoutPullRequestIntoThread, composerTrigger, searchableModelOptions, workspaceEntries]); - const composerMenuOpen = Boolean(composerTrigger) || pullRequestComposerMenuOpen; + }, [composerTrigger, searchableModelOptions, workspaceEntries]); + const composerMenuOpen = Boolean(composerTrigger); const activeComposerMenuItem = useMemo( () => - pullRequestComposerMenuOpen - ? null - : (composerMenuItems.find((item) => item.id === composerHighlightedItemId) ?? - composerMenuItems[0] ?? - null), - [composerHighlightedItemId, composerMenuItems, pullRequestComposerMenuOpen], + composerMenuItems.find((item) => item.id === composerHighlightedItemId) ?? + composerMenuItems[0] ?? + null, + [composerHighlightedItemId, composerMenuItems], ); composerMenuOpenRef.current = composerMenuOpen; composerMenuItemsRef.current = composerMenuItems; @@ -2055,7 +2043,7 @@ export default function ChatView({ threadId }: ChatViewProps) { useEffect(() => { setExpandedWorkGroups({}); - setPullRequestComposerMenuState(null); + setPullRequestDialogState(null); if (planSidebarOpenOnNextThreadRef.current) { planSidebarOpenOnNextThreadRef.current = false; setPlanSidebarOpen(true); @@ -2070,16 +2058,12 @@ export default function ChatView({ threadId }: ChatViewProps) { setComposerHighlightedItemId(null); return; } - if (pullRequestComposerMenuOpen) { - setComposerHighlightedItemId(null); - return; - } setComposerHighlightedItemId((existing) => existing && composerMenuItems.some((item) => item.id === existing) ? existing : (composerMenuItems[0]?.id ?? null), ); - }, [composerMenuItems, composerMenuOpen, pullRequestComposerMenuOpen]); + }, [composerMenuItems, composerMenuOpen]); useEffect(() => { setIsRevertingCheckpoint(false); @@ -2589,15 +2573,8 @@ export default function ChatView({ threadId }: ChatViewProps) { } const standaloneSlashCommand = composerImages.length === 0 ? parseStandaloneComposerSlashCommand(trimmed) : null; - const shouldHandleStandaloneSlashCommand = - standaloneSlashCommand !== null && - (standaloneSlashCommand !== "checkout-pr" || canCheckoutPullRequestIntoThread); - if (shouldHandleStandaloneSlashCommand && standaloneSlashCommand) { - if (standaloneSlashCommand === "checkout-pr") { - openPullRequestComposerMenu(); - } else { - await handleInteractionModeChange(standaloneSlashCommand); - } + if (standaloneSlashCommand) { + await handleInteractionModeChange(standaloneSlashCommand); promptRef.current = ""; clearComposerDraftContent(activeThread.id); setComposerHighlightedItemId(null); @@ -3386,11 +3363,7 @@ export default function ChatView({ threadId }: ChatViewProps) { } return; } - if (item.command === "checkout-pr") { - openPullRequestComposerMenu(); - } else { - void handleInteractionModeChange(item.command === "plan" ? "plan" : "default"); - } + void handleInteractionModeChange(item.command === "plan" ? "plan" : "default"); const applied = applyPromptReplacement(trigger.rangeStart, trigger.rangeEnd, "", { expectedText: expectedToken, }); @@ -3410,7 +3383,6 @@ export default function ChatView({ threadId }: ChatViewProps) { [ applyPromptReplacement, handleInteractionModeChange, - openPullRequestComposerMenu, onProviderModelSelect, resolveActiveComposerTrigger, ], @@ -3485,12 +3457,6 @@ export default function ChatView({ threadId }: ChatViewProps) { const { trigger } = resolveActiveComposerTrigger(); const menuIsActive = composerMenuOpenRef.current || trigger !== null; - if (pullRequestComposerMenuOpen) { - if (key === "ArrowDown" || key === "ArrowUp" || key === "Tab" || key === "Enter") { - return true; - } - } - if (menuIsActive) { const currentItems = composerMenuItemsRef.current; if (key === "ArrowDown" && currentItems.length > 0) { @@ -3712,28 +3678,15 @@ export default function ChatView({ threadId }: ChatViewProps) { > {composerMenuOpen && !isComposerApprovalState && (
- {pullRequestComposerMenuState ? ( - { - await handlePreparedPullRequestThread(input); - closePullRequestComposerMenu(); - }} - /> - ) : ( - - )} +
)} @@ -4160,10 +4113,24 @@ export default function ChatView({ threadId }: ChatViewProps) { envLocked={envLocked} onComposerFocusRequest={scheduleComposerFocus} {...(canCheckoutPullRequestIntoThread - ? { onCheckoutPullRequestRequest: openPullRequestComposerMenu } + ? { onCheckoutPullRequestRequest: openPullRequestDialog } : {})} /> )} + {pullRequestDialogState ? ( + { + if (!open) { + closePullRequestDialog(); + } + }} + onPrepared={handlePreparedPullRequestThread} + /> + ) : null} {/* end chat column */} {/* Plan sidebar */} diff --git a/apps/web/src/components/ComposerPullRequestCommandMenu.tsx b/apps/web/src/components/ComposerPullRequestCommandMenu.tsx deleted file mode 100644 index 53dd7ab2ea363c5d8e3d72ce150c1ba7a0252e19..0000000000000000000000000000000000000000 --- a/apps/web/src/components/ComposerPullRequestCommandMenu.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import type { GitResolvePullRequestResult } from "@t3tools/contracts"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useDebouncedValue } from "@tanstack/react-pacer"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; - -import { - gitPreparePullRequestThreadMutationOptions, - gitResolvePullRequestQueryOptions, -} from "~/lib/gitReactQuery"; -import { cn } from "~/lib/utils"; -import { parsePullRequestReference } from "~/pullRequestReference"; -import { Button } from "./ui/button"; -import { Input } from "./ui/input"; -import { Spinner } from "./ui/spinner"; - -interface ComposerPullRequestCommandMenuProps { - cwd: string | null; - initialReference: string | null; - onCancel: () => void; - onPrepared: (input: { branch: string; worktreePath: string | null }) => Promise | void; -} - -export function ComposerPullRequestCommandMenu({ - cwd, - initialReference, - onCancel, - onPrepared, -}: ComposerPullRequestCommandMenuProps) { - const queryClient = useQueryClient(); - const referenceInputRef = useRef(null); - const [reference, setReference] = useState(initialReference ?? ""); - const [referenceDirty, setReferenceDirty] = useState(false); - const [preparingMode, setPreparingMode] = useState<"local" | "worktree" | null>(null); - const [debouncedReference, referenceDebouncer] = useDebouncedValue( - reference, - { wait: 450 }, - (debouncerState) => ({ isPending: debouncerState.isPending }), - ); - - useEffect(() => { - setReference(initialReference ?? ""); - setReferenceDirty(false); - setPreparingMode(null); - }, [initialReference]); - - useEffect(() => { - const frame = window.requestAnimationFrame(() => { - referenceInputRef.current?.focus(); - referenceInputRef.current?.select(); - }); - return () => { - window.cancelAnimationFrame(frame); - }; - }, []); - - const parsedReference = parsePullRequestReference(reference); - const parsedDebouncedReference = parsePullRequestReference(debouncedReference); - const resolvePullRequestQuery = useQuery( - gitResolvePullRequestQueryOptions({ - cwd, - reference: parsedDebouncedReference, - }), - ); - const cachedPullRequest = useMemo(() => { - if (!cwd || !parsedReference) { - return null; - } - const cached = queryClient.getQueryData([ - "git", - "pull-request", - cwd, - parsedReference, - ]); - return cached?.pullRequest ?? null; - }, [cwd, parsedReference, queryClient]); - const preparePullRequestThreadMutation = useMutation( - gitPreparePullRequestThreadMutationOptions({ cwd, queryClient }), - ); - - const liveResolvedPullRequest = - parsedReference !== null && parsedReference === parsedDebouncedReference - ? (resolvePullRequestQuery.data?.pullRequest ?? null) - : null; - const resolvedPullRequest = liveResolvedPullRequest ?? cachedPullRequest; - const isResolving = - parsedReference !== null && - resolvedPullRequest === null && - (referenceDebouncer.state.isPending || - parsedReference !== parsedDebouncedReference || - resolvePullRequestQuery.isPending || - resolvePullRequestQuery.isFetching); - const statusTone = useMemo(() => { - switch (resolvedPullRequest?.state) { - case "merged": - return "text-violet-600 dark:text-violet-300/90"; - case "closed": - return "text-zinc-500 dark:text-zinc-400/80"; - case "open": - return "text-emerald-600 dark:text-emerald-300/90"; - default: - return "text-muted-foreground"; - } - }, [resolvedPullRequest?.state]); - - const handleConfirm = useCallback(async (mode: "local" | "worktree") => { - if (!parsedReference) { - setReferenceDirty(true); - return; - } - if (!parsedReference || !resolvedPullRequest || !cwd) { - return; - } - setPreparingMode(mode); - try { - const result = await preparePullRequestThreadMutation.mutateAsync({ - reference: parsedReference, - mode, - }); - await onPrepared({ - branch: result.branch, - worktreePath: result.worktreePath, - }); - } finally { - setPreparingMode(null); - } - }, [ - cwd, - onPrepared, - parsedReference, - preparePullRequestThreadMutation, - resolvedPullRequest, - ]); - - const validationMessage = - !referenceDirty - ? null - : reference.trim().length === 0 - ? "Paste a GitHub pull request URL or enter 123 / #123." - : parsedReference === null - ? "Use a GitHub pull request URL, 123, or #123." - : null; - const errorMessage = - validationMessage ?? - (resolvedPullRequest === null && resolvePullRequestQuery.isError - ? (resolvePullRequestQuery.error instanceof Error - ? resolvePullRequestQuery.error.message - : "Failed to resolve pull request.") - : (preparePullRequestThreadMutation.error instanceof Error - ? preparePullRequestThreadMutation.error.message - : preparePullRequestThreadMutation.error - ? "Failed to prepare pull request thread." - : null)); - - return ( -
-
-
-
-
-

Checkout Pull Request

-

- Resolve a PR, then choose Local or Worktree for the new draft thread. -

-
-
- { - setReferenceDirty(true); - setReference(event.target.value); - }} - onKeyDown={(event) => { - if (event.key !== "Enter") { - return; - } - event.preventDefault(); - if (!isResolving && !preparePullRequestThreadMutation.isPending) { - void handleConfirm("local"); - } - }} - /> -
- - {resolvedPullRequest ? ( -
-
-
-

{resolvedPullRequest.title}

-

- #{resolvedPullRequest.number} · {resolvedPullRequest.headBranch} to{" "} - {resolvedPullRequest.baseBranch} -

-
- - {resolvedPullRequest.state} - -
-
- ) : null} - - {isResolving ? ( -
- - Resolving pull request... -
- ) : null} - - {errorMessage ?

{errorMessage}

: null} -
- -
-

- {resolvedPullRequest - ? "Create the draft thread in the main repo or in a dedicated worktree." - : "Resolve the pull request first."} -

-
- - - -
-
-
- ); -} diff --git a/apps/web/src/components/PullRequestThreadDialog.tsx b/apps/web/src/components/PullRequestThreadDialog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ced24aca7fcc38b73e13213cb0e7cab4e89373ad --- /dev/null +++ b/apps/web/src/components/PullRequestThreadDialog.tsx @@ -0,0 +1,281 @@ +import type { GitResolvePullRequestResult } from "@t3tools/contracts"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useDebouncedValue } from "@tanstack/react-pacer"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; + +import { + gitPreparePullRequestThreadMutationOptions, + gitResolvePullRequestQueryOptions, +} from "~/lib/gitReactQuery"; +import { cn } from "~/lib/utils"; +import { parsePullRequestReference } from "~/pullRequestReference"; +import { Button } from "./ui/button"; +import { + Dialog, + DialogDescription, + DialogFooter, + DialogHeader, + DialogPanel, + DialogPopup, + DialogTitle, +} from "./ui/dialog"; +import { Input } from "./ui/input"; +import { Spinner } from "./ui/spinner"; + +interface PullRequestThreadDialogProps { + open: boolean; + cwd: string | null; + initialReference: string | null; + onOpenChange: (open: boolean) => void; + onPrepared: (input: { branch: string; worktreePath: string | null }) => Promise | void; +} + +export function PullRequestThreadDialog({ + open, + cwd, + initialReference, + onOpenChange, + onPrepared, +}: PullRequestThreadDialogProps) { + const queryClient = useQueryClient(); + const referenceInputRef = useRef(null); + const [reference, setReference] = useState(initialReference ?? ""); + const [referenceDirty, setReferenceDirty] = useState(false); + const [preparingMode, setPreparingMode] = useState<"local" | "worktree" | null>(null); + const [debouncedReference, referenceDebouncer] = useDebouncedValue( + reference, + { wait: 450 }, + (debouncerState) => ({ isPending: debouncerState.isPending }), + ); + + useEffect(() => { + if (!open) return; + setReference(initialReference ?? ""); + setReferenceDirty(false); + setPreparingMode(null); + }, [initialReference, open]); + + useEffect(() => { + if (!open) return; + const frame = window.requestAnimationFrame(() => { + referenceInputRef.current?.focus(); + referenceInputRef.current?.select(); + }); + return () => { + window.cancelAnimationFrame(frame); + }; + }, [open]); + + const parsedReference = parsePullRequestReference(reference); + const parsedDebouncedReference = parsePullRequestReference(debouncedReference); + const resolvePullRequestQuery = useQuery( + gitResolvePullRequestQueryOptions({ + cwd, + reference: open ? parsedDebouncedReference : null, + }), + ); + const cachedPullRequest = useMemo(() => { + if (!cwd || !parsedReference) { + return null; + } + const cached = queryClient.getQueryData([ + "git", + "pull-request", + cwd, + parsedReference, + ]); + return cached?.pullRequest ?? null; + }, [cwd, parsedReference, queryClient]); + const preparePullRequestThreadMutation = useMutation( + gitPreparePullRequestThreadMutationOptions({ cwd, queryClient }), + ); + + const liveResolvedPullRequest = + parsedReference !== null && parsedReference === parsedDebouncedReference + ? (resolvePullRequestQuery.data?.pullRequest ?? null) + : null; + const resolvedPullRequest = liveResolvedPullRequest ?? cachedPullRequest; + const isResolving = + open && + parsedReference !== null && + resolvedPullRequest === null && + (referenceDebouncer.state.isPending || + parsedReference !== parsedDebouncedReference || + resolvePullRequestQuery.isPending || + resolvePullRequestQuery.isFetching); + const statusTone = useMemo(() => { + switch (resolvedPullRequest?.state) { + case "merged": + return "text-violet-600 dark:text-violet-300/90"; + case "closed": + return "text-zinc-500 dark:text-zinc-400/80"; + case "open": + return "text-emerald-600 dark:text-emerald-300/90"; + default: + return "text-muted-foreground"; + } + }, [resolvedPullRequest?.state]); + + const handleConfirm = useCallback(async (mode: "local" | "worktree") => { + if (!parsedReference) { + setReferenceDirty(true); + return; + } + if (!parsedReference || !resolvedPullRequest || !cwd) { + return; + } + setPreparingMode(mode); + try { + const result = await preparePullRequestThreadMutation.mutateAsync({ + reference: parsedReference, + mode, + }); + await onPrepared({ + branch: result.branch, + worktreePath: result.worktreePath, + }); + onOpenChange(false); + } finally { + setPreparingMode(null); + } + }, [ + cwd, + onOpenChange, + onPrepared, + parsedReference, + preparePullRequestThreadMutation, + resolvedPullRequest, + ]); + + const validationMessage = + !referenceDirty + ? null + : reference.trim().length === 0 + ? "Paste a GitHub pull request URL or enter 123 / #123." + : parsedReference === null + ? "Use a GitHub pull request URL, 123, or #123." + : null; + const errorMessage = + validationMessage ?? + (resolvedPullRequest === null && resolvePullRequestQuery.isError + ? (resolvePullRequestQuery.error instanceof Error + ? resolvePullRequestQuery.error.message + : "Failed to resolve pull request.") + : (preparePullRequestThreadMutation.error instanceof Error + ? preparePullRequestThreadMutation.error.message + : preparePullRequestThreadMutation.error + ? "Failed to prepare pull request thread." + : null)); + + return ( + { + if (!preparePullRequestThreadMutation.isPending) { + onOpenChange(nextOpen); + } + }} + > + + + Checkout Pull Request + + Resolve a GitHub pull request, then create the draft thread in the main repo or in a + dedicated worktree. + + + + + + {resolvedPullRequest ? ( +
+
+
+

{resolvedPullRequest.title}

+

+ #{resolvedPullRequest.number} · {resolvedPullRequest.headBranch} to{" "} + {resolvedPullRequest.baseBranch} +

+
+ + {resolvedPullRequest.state} + +
+
+ ) : null} + + {isResolving ? ( +
+ + Resolving pull request... +
+ ) : null} + + {errorMessage ?

{errorMessage}

: null} +
+ + + + + +
+
+ ); +} diff --git a/apps/web/src/composer-logic.test.ts b/apps/web/src/composer-logic.test.ts index ef490f6e954109956def53812378420695e0d4a0..4e4bd145d7bb3177ecfc4c4e89f4d7eef23caf04 100644 --- a/apps/web/src/composer-logic.test.ts +++ b/apps/web/src/composer-logic.test.ts @@ -134,11 +134,11 @@ describe("parseStandaloneComposerSlashCommand", () => { expect(parseStandaloneComposerSlashCommand("/default")).toBe("default"); }); - it("parses standalone /checkout-pr command", () => { - expect(parseStandaloneComposerSlashCommand(" /checkout-pr ")).toBe("checkout-pr"); - }); - it("ignores slash commands with extra message text", () => { expect(parseStandaloneComposerSlashCommand("/plan explain this")).toBeNull(); }); + + it("ignores removed /checkout-pr command", () => { + expect(parseStandaloneComposerSlashCommand(" /checkout-pr ")).toBeNull(); + }); }); diff --git a/apps/web/src/composer-logic.ts b/apps/web/src/composer-logic.ts index cde009708b33e8c996308e6a0e54f3d97080dff4..9bb30132a76444604bc3530209f058f12944a933 100644 --- a/apps/web/src/composer-logic.ts +++ b/apps/web/src/composer-logic.ts @@ -1,7 +1,7 @@ import { splitPromptIntoComposerSegments } from "./composer-editor-mentions"; export type ComposerTriggerKind = "path" | "slash-command" | "slash-model"; -export type ComposerSlashCommand = "model" | "plan" | "default" | "checkout-pr"; +export type ComposerSlashCommand = "model" | "plan" | "default"; export interface ComposerTrigger { kind: ComposerTriggerKind; @@ -14,7 +14,6 @@ const SLASH_COMMANDS: readonly ComposerSlashCommand[] = [ "model", "plan", "default", - "checkout-pr", ]; function clampCursor(text: string, cursor: number): number { @@ -169,13 +168,12 @@ export function parseStandaloneComposerSlashCommand(text: string): Exclude< ComposerSlashCommand, "model" > | null { - const match = /^\/(plan|default|checkout-pr)\s*$/i.exec(text.trim()); + const match = /^\/(plan|default)\s*$/i.exec(text.trim()); if (!match) { return null; } const command = match[1]?.toLowerCase(); if (command === "plan") return "plan"; - if (command === "checkout-pr") return "checkout-pr"; return "default"; }