authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-09 11:56:11-07:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-09 22:50:45-07:00
logf70cb5de4517e55f62ed2c71e49291d97a7dc927
treedde4d5ad7b24bbcc43cc2325c09619711b0bbbf9
parent3d3586800ebfbefb97a4a782dae3439e90ba9065

Refine branch picker behavior and gate /checkout-pr by thread type

- Add shared branch picker filtering logic and tests, including PR-reference-only results - Improve branch selector UX/perf with deferred query filtering, conditional virtualization, and status-based current branch resolution - Only expose and handle `/checkout-pr` for local draft threads in chat and toolbar UI

4 files changed, 206 insertions(+), 125 deletions(-)

apps/web/src/components/BranchToolbar.logic.test.ts+29
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import type { GitBranch } from "@t3tools/contracts";1import type { GitBranch } from "@t3tools/contracts";
2import { describe, expect, it } from "vitest";2import { describe, expect, it } from "vitest";
3import {3import {
4 filterBranchPickerItems,
4 dedupeRemoteBranchesWithLocalMatches,5 dedupeRemoteBranchesWithLocalMatches,
5 deriveLocalBranchNameFromRemoteRef,6 deriveLocalBranchNameFromRemoteRef,
6 resolveDraftEnvModeAfterBranchChange,7 resolveDraftEnvModeAfterBranchChange,
...@@ -194,3 +195,31 @@ describe("dedupeRemoteBranchesWithLocalMatches", () => {...@@ -194,3 +195,31 @@ describe("dedupeRemoteBranchesWithLocalMatches", () => {
194 ]);195 ]);
195 });196 });
196});197});
198
199describe("filterBranchPickerItems", () => {
200 it("shows only the pull request action when the query is a PR reference", () => {
201 expect(
202 filterBranchPickerItems({
203 itemValues: [
204 "__checkout_pull_request__:488",
205 "codething/76f09488",
206 "origin/cursor/rollback-error",
207 ],
208 normalizedQuery: "488",
209 createBranchItemValue: null,
210 checkoutPullRequestItemValue: "__checkout_pull_request__:488",
211 }),
212 ).toEqual(["__checkout_pull_request__:488"]);
213 });
214
215 it("filters branch items normally when the query is not a PR reference", () => {
216 expect(
217 filterBranchPickerItems({
218 itemValues: ["feature/488", "main", "__create_new_branch__:48"],
219 normalizedQuery: "48",
220 createBranchItemValue: "__create_new_branch__:48",
221 checkoutPullRequestItemValue: null,
222 }),
223 ).toEqual(["feature/488", "__create_new_branch__:48"]);
224 });
225});
apps/web/src/components/BranchToolbar.logic.ts+24
...@@ -88,3 +88,27 @@ export function dedupeRemoteBranchesWithLocalMatches(...@@ -88,3 +88,27 @@ export function dedupeRemoteBranchesWithLocalMatches(
88 return !localBranchCandidates.some((candidate) => localBranchNames.has(candidate));88 return !localBranchCandidates.some((candidate) => localBranchNames.has(candidate));
89 });89 });
90}90}
91
92export function filterBranchPickerItems(input: {
93 itemValues: ReadonlyArray<string>;
94 normalizedQuery: string;
95 createBranchItemValue: string | null;
96 checkoutPullRequestItemValue: string | null;
97}): ReadonlyArray<string> {
98 const { itemValues, normalizedQuery, createBranchItemValue, checkoutPullRequestItemValue } = input;
99
100 if (checkoutPullRequestItemValue) {
101 return [checkoutPullRequestItemValue];
102 }
103
104 if (normalizedQuery.length === 0) {
105 return itemValues;
106 }
107
108 return itemValues.filter((itemValue) => {
109 if (createBranchItemValue && itemValue === createBranchItemValue) {
110 return true;
111 }
112 return itemValue.toLowerCase().includes(normalizedQuery);
113 });
114}
apps/web/src/components/BranchToolbarBranchSelector.tsx+138-120
...@@ -3,7 +3,9 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";...@@ -3,7 +3,9 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
3import { useVirtualizer } from "@tanstack/react-virtual";3import { useVirtualizer } from "@tanstack/react-virtual";
4import { ChevronDownIcon } from "lucide-react";4import { ChevronDownIcon } from "lucide-react";
5import {5import {
6 type CSSProperties,
6 useCallback,7 useCallback,
8 useDeferredValue,
7 useEffect,9 useEffect,
8 useMemo,10 useMemo,
9 useOptimistic,11 useOptimistic,
...@@ -12,13 +14,19 @@ import {...@@ -12,13 +14,19 @@ import {
12 useTransition,14 useTransition,
13} from "react";15} from "react";
1416
15import { gitBranchesQueryOptions, gitQueryKeys, invalidateGitQueries } from "../lib/gitReactQuery";17import {
18 gitBranchesQueryOptions,
19 gitQueryKeys,
20 gitStatusQueryOptions,
21 invalidateGitQueries,
22} from "../lib/gitReactQuery";
16import { readNativeApi } from "../nativeApi";23import { readNativeApi } from "../nativeApi";
17import { parsePullRequestReference } from "../pullRequestReference";24import { parsePullRequestReference } from "../pullRequestReference";
18import {25import {
19 dedupeRemoteBranchesWithLocalMatches,26 dedupeRemoteBranchesWithLocalMatches,
20 deriveLocalBranchNameFromRemoteRef,27 deriveLocalBranchNameFromRemoteRef,
21 EnvMode,28 EnvMode,
29 filterBranchPickerItems,
22 resolveBranchToolbarValue,30 resolveBranchToolbarValue,
23} from "./BranchToolbar.logic";31} from "./BranchToolbar.logic";
24import { Button } from "./ui/button";32import { Button } from "./ui/button";
...@@ -78,13 +86,16 @@ export function BranchToolbarBranchSelector({...@@ -78,13 +86,16 @@ export function BranchToolbarBranchSelector({
78 const queryClient = useQueryClient();86 const queryClient = useQueryClient();
79 const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false);87 const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false);
80 const [branchQuery, setBranchQuery] = useState("");88 const [branchQuery, setBranchQuery] = useState("");
89 const deferredBranchQuery = useDeferredValue(branchQuery);
8190
82 const branchesQuery = useQuery(gitBranchesQueryOptions(branchCwd));91 const branchesQuery = useQuery(gitBranchesQueryOptions(branchCwd));
92 const branchStatusQuery = useQuery(gitStatusQueryOptions(branchCwd));
83 const branches = useMemo(93 const branches = useMemo(
84 () => dedupeRemoteBranchesWithLocalMatches(branchesQuery.data?.branches ?? []),94 () => dedupeRemoteBranchesWithLocalMatches(branchesQuery.data?.branches ?? []),
85 [branchesQuery.data?.branches],95 [branchesQuery.data?.branches],
86 );96 );
87 const currentGitBranch = branches.find((branch) => branch.current)?.name ?? null;97 const currentGitBranch =
98 branchStatusQuery.data?.branch ?? branches.find((branch) => branch.current)?.name ?? null;
88 const canonicalActiveBranch = resolveBranchToolbarValue({99 const canonicalActiveBranch = resolveBranchToolbarValue({
89 envMode: effectiveEnvMode,100 envMode: effectiveEnvMode,
90 activeWorktreePath,101 activeWorktreePath,
...@@ -97,12 +108,15 @@ export function BranchToolbarBranchSelector({...@@ -97,12 +108,15 @@ export function BranchToolbarBranchSelector({
97 [branches],108 [branches],
98 );109 );
99 const trimmedBranchQuery = branchQuery.trim();110 const trimmedBranchQuery = branchQuery.trim();
100 const normalizedBranchQuery = trimmedBranchQuery.toLowerCase();111 const deferredTrimmedBranchQuery = deferredBranchQuery.trim();
112 const normalizedDeferredBranchQuery = deferredTrimmedBranchQuery.toLowerCase();
101 const prReference = parsePullRequestReference(trimmedBranchQuery);113 const prReference = parsePullRequestReference(trimmedBranchQuery);
114 const isSelectingWorktreeBase =
115 effectiveEnvMode === "worktree" && !envLocked && !activeWorktreePath;
102 const checkoutPullRequestItemValue = prReference && onCheckoutPullRequestRequest116 const checkoutPullRequestItemValue = prReference && onCheckoutPullRequestRequest
103 ? `__checkout_pull_request__:${prReference}`117 ? `__checkout_pull_request__:${prReference}`
104 : null;118 : null;
105 const canCreateBranch = effectiveEnvMode === "local" && trimmedBranchQuery.length > 0;119 const canCreateBranch = !isSelectingWorktreeBase && trimmedBranchQuery.length > 0;
106 const hasExactBranchMatch = branchByName.has(trimmedBranchQuery);120 const hasExactBranchMatch = branchByName.has(trimmedBranchQuery);
107 const createBranchItemValue = canCreateBranch121 const createBranchItemValue = canCreateBranch
108 ? `__create_new_branch__:${trimmedBranchQuery}`122 ? `__create_new_branch__:${trimmedBranchQuery}`
...@@ -122,19 +136,25 @@ export function BranchToolbarBranchSelector({...@@ -122,19 +136,25 @@ export function BranchToolbarBranchSelector({
122 );136 );
123 const filteredBranchPickerItems = useMemo(137 const filteredBranchPickerItems = useMemo(
124 () =>138 () =>
125 normalizedBranchQuery.length === 0139 filterBranchPickerItems({
126 ? branchPickerItems140 itemValues: branchPickerItems,
127 : branchPickerItems.filter((itemValue) => {141 normalizedQuery: normalizedDeferredBranchQuery,
128 if (createBranchItemValue && itemValue === createBranchItemValue) return true;142 createBranchItemValue,
129 return itemValue.toLowerCase().includes(normalizedBranchQuery);143 checkoutPullRequestItemValue,
130 }),144 }),
131 [branchPickerItems, createBranchItemValue, normalizedBranchQuery],145 [
146 branchPickerItems,
147 checkoutPullRequestItemValue,
148 createBranchItemValue,
149 normalizedDeferredBranchQuery,
150 ],
132 );151 );
133 const [resolvedActiveBranch, setOptimisticBranch] = useOptimistic(152 const [resolvedActiveBranch, setOptimisticBranch] = useOptimistic(
134 canonicalActiveBranch,153 canonicalActiveBranch,
135 (_currentBranch: string | null, optimisticBranch: string | null) => optimisticBranch,154 (_currentBranch: string | null, optimisticBranch: string | null) => optimisticBranch,
136 );155 );
137 const [isBranchActionPending, startBranchActionTransition] = useTransition();156 const [isBranchActionPending, startBranchActionTransition] = useTransition();
157 const shouldVirtualizeBranchList = filteredBranchPickerItems.length > 40;
138158
139 const runBranchAction = (action: () => Promise<void>) => {159 const runBranchAction = (action: () => Promise<void>) => {
140 startBranchActionTransition(async () => {160 startBranchActionTransition(async () => {
...@@ -148,7 +168,7 @@ export function BranchToolbarBranchSelector({...@@ -148,7 +168,7 @@ export function BranchToolbarBranchSelector({
148 if (!api || !branchCwd || isBranchActionPending) return;168 if (!api || !branchCwd || isBranchActionPending) return;
149169
150 // In new-worktree mode, selecting a branch sets the base branch.170 // In new-worktree mode, selecting a branch sets the base branch.
151 if (effectiveEnvMode === "worktree" && !envLocked && !activeWorktreePath) {171 if (isSelectingWorktreeBase) {
152 onSetThreadBranch(branch.name, null);172 onSetThreadBranch(branch.name, null);
153 setIsBranchMenuOpen(false);173 setIsBranchMenuOpen(false);
154 onComposerFocusRequest?.();174 onComposerFocusRequest?.();
...@@ -274,7 +294,7 @@ export function BranchToolbarBranchSelector({...@@ -274,7 +294,7 @@ export function BranchToolbarBranchSelector({
274 estimateSize: () => 28,294 estimateSize: () => 28,
275 getScrollElement: () => branchListScrollElementRef.current,295 getScrollElement: () => branchListScrollElementRef.current,
276 overscan: 12,296 overscan: 12,
277 enabled: isBranchMenuOpen,297 enabled: isBranchMenuOpen && shouldVirtualizeBranchList,
278 initialRect: {298 initialRect: {
279 height: 224,299 height: 224,
280 width: 0,300 width: 0,
...@@ -293,11 +313,16 @@ export function BranchToolbarBranchSelector({...@@ -293,11 +313,16 @@ export function BranchToolbarBranchSelector({
293 );313 );
294314
295 useEffect(() => {315 useEffect(() => {
296 if (!isBranchMenuOpen) return;316 if (!isBranchMenuOpen || !shouldVirtualizeBranchList) return;
297 queueMicrotask(() => {317 queueMicrotask(() => {
298 branchListVirtualizer.measure();318 branchListVirtualizer.measure();
299 });319 });
300 }, [branchListVirtualizer, filteredBranchPickerItems.length, isBranchMenuOpen]);320 }, [
321 branchListVirtualizer,
322 filteredBranchPickerItems.length,
323 isBranchMenuOpen,
324 shouldVirtualizeBranchList,
325 ]);
301326
302 const triggerLabel = getBranchTriggerLabel({327 const triggerLabel = getBranchTriggerLabel({
303 activeWorktreePath,328 activeWorktreePath,
...@@ -305,6 +330,80 @@ export function BranchToolbarBranchSelector({...@@ -305,6 +330,80 @@ export function BranchToolbarBranchSelector({
305 resolvedActiveBranch,330 resolvedActiveBranch,
306 });331 });
307332
333 function renderPickerItem(itemValue: string, index: number, style?: CSSProperties) {
334 if (checkoutPullRequestItemValue && itemValue === checkoutPullRequestItemValue) {
335 return (
336 <ComboboxItem
337 hideIndicator
338 key={itemValue}
339 index={index}
340 value={itemValue}
341 style={style}
342 onClick={() => {
343 if (!prReference || !onCheckoutPullRequestRequest) {
344 return;
345 }
346 setIsBranchMenuOpen(false);
347 setBranchQuery("");
348 onComposerFocusRequest?.();
349 onCheckoutPullRequestRequest(prReference);
350 }}
351 >
352 <div className="flex min-w-0 flex-col items-start py-1">
353 <span className="truncate font-medium">Checkout Pull Request</span>
354 <span className="truncate text-muted-foreground text-xs">{prReference}</span>
355 </div>
356 </ComboboxItem>
357 );
358 }
359 if (createBranchItemValue && itemValue === createBranchItemValue) {
360 return (
361 <ComboboxItem
362 hideIndicator
363 key={itemValue}
364 index={index}
365 value={itemValue}
366 style={style}
367 onClick={() => createBranch(trimmedBranchQuery)}
368 >
369 <span className="truncate">Create new branch "{trimmedBranchQuery}"</span>
370 </ComboboxItem>
371 );
372 }
373
374 const branch = branchByName.get(itemValue);
375 if (!branch) return null;
376
377 const hasSecondaryWorktree = branch.worktreePath && branch.worktreePath !== activeProjectCwd;
378 const badge = branch.current
379 ? "current"
380 : hasSecondaryWorktree
381 ? "worktree"
382 : branch.isRemote
383 ? "remote"
384 : branch.isDefault
385 ? "default"
386 : null;
387 return (
388 <ComboboxItem
389 hideIndicator
390 key={itemValue}
391 index={index}
392 value={itemValue}
393 className={itemValue === resolvedActiveBranch ? "bg-accent text-foreground" : undefined}
394 style={style}
395 onClick={() => selectBranch(branch)}
396 >
397 <div className="flex w-full items-center justify-between gap-2">
398 <span className="truncate">{itemValue}</span>
399 {badge && (
400 <span className="shrink-0 text-[10px] text-muted-foreground/45">{badge}</span>
401 )}
402 </div>
403 </ComboboxItem>
404 );
405 }
406
308 return (407 return (
309 <Combobox408 <Combobox
310 items={branchPickerItems}409 items={branchPickerItems}
...@@ -322,12 +421,12 @@ export function BranchToolbarBranchSelector({...@@ -322,12 +421,12 @@ export function BranchToolbarBranchSelector({
322 <ComboboxTrigger421 <ComboboxTrigger
323 render={<Button variant="ghost" size="xs" />}422 render={<Button variant="ghost" size="xs" />}
324 className="text-muted-foreground/70 hover:text-foreground/80"423 className="text-muted-foreground/70 hover:text-foreground/80"
325 disabled={branchesQuery.isLoading || isBranchActionPending}424 disabled={(branchesQuery.isLoading && branches.length === 0) || isBranchActionPending}
326 >425 >
327 <span className="max-w-[240px] truncate">{triggerLabel}</span>426 <span className="max-w-[240px] truncate">{triggerLabel}</span>
328 <ChevronDownIcon />427 <ChevronDownIcon />
329 </ComboboxTrigger>428 </ComboboxTrigger>
330 <ComboboxPopup align="end" side="top" className="w-64">429 <ComboboxPopup align="end" side="top" className="w-80">
331 <div className="border-b p-1">430 <div className="border-b p-1">
332 <ComboboxInput431 <ComboboxInput
333 className="[&_input]:font-sans rounded-md"432 className="[&_input]:font-sans rounded-md"
...@@ -342,109 +441,28 @@ export function BranchToolbarBranchSelector({...@@ -342,109 +441,28 @@ export function BranchToolbarBranchSelector({
342 <ComboboxEmpty>No branches found.</ComboboxEmpty>441 <ComboboxEmpty>No branches found.</ComboboxEmpty>
343442
344 <ComboboxList ref={setBranchListRef} className="max-h-56">443 <ComboboxList ref={setBranchListRef} className="max-h-56">
345 <div444 {shouldVirtualizeBranchList ? (
346 className="relative"445 <div
347 style={{446 className="relative"
348 height: `${branchListVirtualizer.getTotalSize()}px`,447 style={{
349 }}448 height: `${branchListVirtualizer.getTotalSize()}px`,
350 >449 }}
351 {virtualBranchRows.map((virtualRow) => {450 >
352 const itemValue = filteredBranchPickerItems[virtualRow.index];451 {virtualBranchRows.map((virtualRow) => {
353 if (!itemValue) return null;452 const itemValue = filteredBranchPickerItems[virtualRow.index];
354 if (checkoutPullRequestItemValue && itemValue === checkoutPullRequestItemValue) {453 if (!itemValue) return null;
355 return (454 return renderPickerItem(itemValue, virtualRow.index, {
356 <ComboboxItem455 position: "absolute",
357 hideIndicator456 top: 0,
358 key={itemValue}457 left: 0,
359 index={virtualRow.index}458 width: "100%",
360 value={itemValue}459 transform: `translateY(${virtualRow.start}px)`,
361 style={{460 });
362 position: "absolute",461 })}
363 top: 0,462 </div>
364 left: 0,463 ) : (
365 width: "100%",464 filteredBranchPickerItems.map((itemValue, index) => renderPickerItem(itemValue, index))
366 transform: `translateY(${virtualRow.start}px)`,465 )}
367 }}
368 onClick={() => {
369 if (!prReference || !onCheckoutPullRequestRequest) {
370 return;
371 }
372 setIsBranchMenuOpen(false);
373 setBranchQuery("");
374 onComposerFocusRequest?.();
375 onCheckoutPullRequestRequest(prReference);
376 }}
377 >
378 <div className="flex min-w-0 flex-col items-start">
379 <span className="truncate font-medium">Checkout Pull Request</span>
380 <span className="truncate text-muted-foreground text-xs">{prReference}</span>
381 </div>
382 </ComboboxItem>
383 );
384 }
385 if (createBranchItemValue && itemValue === createBranchItemValue) {
386 return (
387 <ComboboxItem
388 hideIndicator
389 key={itemValue}
390 index={virtualRow.index}
391 value={itemValue}
392 style={{
393 position: "absolute",
394 top: 0,
395 left: 0,
396 width: "100%",
397 transform: `translateY(${virtualRow.start}px)`,
398 }}
399 onClick={() => createBranch(trimmedBranchQuery)}
400 >
401 <span className="truncate">Create new branch "{trimmedBranchQuery}"</span>
402 </ComboboxItem>
403 );
404 }
405
406 const branch = branchByName.get(itemValue);
407 if (!branch) return null;
408
409 const hasSecondaryWorktree =
410 branch.worktreePath && branch.worktreePath !== activeProjectCwd;
411 const badge = branch.current
412 ? "current"
413 : hasSecondaryWorktree
414 ? "worktree"
415 : branch.isRemote
416 ? "remote"
417 : branch.isDefault
418 ? "default"
419 : null;
420 return (
421 <ComboboxItem
422 hideIndicator
423 key={itemValue}
424 index={virtualRow.index}
425 value={itemValue}
426 className={
427 itemValue === resolvedActiveBranch ? "bg-accent text-foreground" : undefined
428 }
429 style={{
430 position: "absolute",
431 top: 0,
432 left: 0,
433 width: "100%",
434 transform: `translateY(${virtualRow.start}px)`,
435 }}
436 onClick={() => selectBranch(branch)}
437 >
438 <div className="flex w-full items-center justify-between gap-2">
439 <span className="truncate">{itemValue}</span>
440 {badge && (
441 <span className="shrink-0 text-[10px] text-muted-foreground/45">{badge}</span>
442 )}
443 </div>
444 </ComboboxItem>
445 );
446 })}
447 </div>
448 </ComboboxList>466 </ComboboxList>
449 </ComboboxPopup>467 </ComboboxPopup>
450 </Combobox>468 </Combobox>
apps/web/src/components/ChatView.tsx+15-5
...@@ -757,6 +757,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -757,6 +757,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
757 composerDraft.interactionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE;757 composerDraft.interactionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE;
758 const isServerThread = serverThread !== undefined;758 const isServerThread = serverThread !== undefined;
759 const isLocalDraftThread = !isServerThread && localDraftThread !== undefined;759 const isLocalDraftThread = !isServerThread && localDraftThread !== undefined;
760 const canCheckoutPullRequestIntoThread = isLocalDraftThread;
760 const diffOpen = rawSearch.diff === "1";761 const diffOpen = rawSearch.diff === "1";
761 const activeThreadId = activeThread?.id ?? null;762 const activeThreadId = activeThread?.id ?? null;
762 const activeLatestTurn = activeThread?.latestTurn ?? null;763 const activeLatestTurn = activeThread?.latestTurn ?? null;
...@@ -764,12 +765,15 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -764,12 +765,15 @@ export default function ChatView({ threadId }: ChatViewProps) {
764 const activeProject = projects.find((p) => p.id === activeThread?.projectId);765 const activeProject = projects.find((p) => p.id === activeThread?.projectId);
765766
766 const openPullRequestComposerMenu = useCallback((reference?: string) => {767 const openPullRequestComposerMenu = useCallback((reference?: string) => {
768 if (!canCheckoutPullRequestIntoThread) {
769 return;
770 }
767 setPullRequestComposerMenuState({771 setPullRequestComposerMenuState({
768 initialReference: reference ?? null,772 initialReference: reference ?? null,
769 key: Date.now(),773 key: Date.now(),
770 });774 });
771 setComposerHighlightedItemId(null);775 setComposerHighlightedItemId(null);
772 }, []);776 }, [canCheckoutPullRequestIntoThread]);
773777
774 const closePullRequestComposerMenu = useCallback(() => {778 const closePullRequestComposerMenu = useCallback(() => {
775 setPullRequestComposerMenuState(null);779 setPullRequestComposerMenuState(null);
...@@ -1315,11 +1319,14 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1315,11 +1319,14 @@ export default function ChatView({ threadId }: ChatViewProps) {
1315 description: "Create a new thread from a GitHub pull request",1319 description: "Create a new thread from a GitHub pull request",
1316 },1320 },
1317 ] satisfies ReadonlyArray<Extract<ComposerCommandItem, { type: "slash-command" }>>;1321 ] satisfies ReadonlyArray<Extract<ComposerCommandItem, { type: "slash-command" }>>;
1322 const availableSlashCommandItems = canCheckoutPullRequestIntoThread
1323 ? slashCommandItems
1324 : slashCommandItems.filter((item) => item.command !== "checkout-pr");
1318 const query = composerTrigger.query.trim().toLowerCase();1325 const query = composerTrigger.query.trim().toLowerCase();
1319 if (!query) {1326 if (!query) {
1320 return [...slashCommandItems];1327 return [...availableSlashCommandItems];
1321 }1328 }
1322 return slashCommandItems.filter(1329 return availableSlashCommandItems.filter(
1323 (item) => item.command.includes(query) || item.label.slice(1).includes(query),1330 (item) => item.command.includes(query) || item.label.slice(1).includes(query),
1324 );1331 );
1325 }1332 }
...@@ -1340,7 +1347,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1340,7 +1347,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
1340 label: name,1347 label: name,
1341 description: `${providerLabel} · ${slug}`,1348 description: `${providerLabel} · ${slug}`,
1342 }));1349 }));
1343 }, [composerTrigger, searchableModelOptions, workspaceEntries]);1350 }, [canCheckoutPullRequestIntoThread, composerTrigger, searchableModelOptions, workspaceEntries]);
1344 const composerMenuOpen = Boolean(composerTrigger) || pullRequestComposerMenuOpen;1351 const composerMenuOpen = Boolean(composerTrigger) || pullRequestComposerMenuOpen;
1345 const activeComposerMenuItem = useMemo(1352 const activeComposerMenuItem = useMemo(
1346 () =>1353 () =>
...@@ -2582,7 +2589,10 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -2582,7 +2589,10 @@ export default function ChatView({ threadId }: ChatViewProps) {
2582 }2589 }
2583 const standaloneSlashCommand =2590 const standaloneSlashCommand =
2584 composerImages.length === 0 ? parseStandaloneComposerSlashCommand(trimmed) : null;2591 composerImages.length === 0 ? parseStandaloneComposerSlashCommand(trimmed) : null;
2585 if (standaloneSlashCommand) {2592 const shouldHandleStandaloneSlashCommand =
2593 standaloneSlashCommand !== null &&
2594 (standaloneSlashCommand !== "checkout-pr" || canCheckoutPullRequestIntoThread);
2595 if (shouldHandleStandaloneSlashCommand && standaloneSlashCommand) {
2586 if (standaloneSlashCommand === "checkout-pr") {2596 if (standaloneSlashCommand === "checkout-pr") {
2587 openPullRequestComposerMenu();2597 openPullRequestComposerMenu();
2588 } else {2598 } else {