Skip to content

Commit 286cab6

Browse files
committed
Unblock cloud branch picker during slow remote load
1 parent b5ecbfb commit 286cab6

2 files changed

Lines changed: 99 additions & 57 deletions

File tree

apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx

Lines changed: 78 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ const COMBOBOX_LIMIT = 50;
3636
// plain button the combobox's roving focus skips over.
3737
const CREATE_BRANCH_ACTION = "__create_branch__";
3838

39+
// Sentinel for the "Use '<input>' as branch name" action in cloud mode.
40+
// Surfaced as the first list item so it's keyboard-reachable and
41+
// auto-highlighted while the (slow) remote search is still running.
42+
const USE_INPUT_BRANCH_ACTION = "__use_input_branch__";
43+
3944
function LoadingRow({ label }: { label: string }) {
4045
return (
4146
<div className="flex items-center gap-1 px-2 py-1.5 text-muted-foreground text-xs">
@@ -136,8 +141,6 @@ export function BranchSelector({
136141

137142
const branches = isCloudMode ? (cloudBranches ?? []) : localBranches;
138143
const effectiveLoading = loading || (isCloudMode && cloudBranchesLoading);
139-
const cloudStillLoading =
140-
isCloudMode && cloudBranchesLoading && branches.length === 0 && !open;
141144
const branchListLoading = isCloudMode
142145
? !!cloudBranchesLoading
143146
: localBranchesLoading;
@@ -164,6 +167,40 @@ export function BranchSelector({
164167
}),
165168
);
166169

170+
// In local mode, surface in-progress git operations (rebase/merge/etc.) so the
171+
// user understands why there's no current branch and why we won't let them
172+
// checkout a different one — checkout would fail with a hard-to-read git error.
173+
const localBusy = !isSelectionOnly && busyState?.busy === true;
174+
const busyOperationLabel =
175+
localBusy && busyState?.busy
176+
? BUSY_OPERATION_LABEL[busyState.operation]
177+
: null;
178+
179+
const displayText = effectiveLoading
180+
? "Loading..."
181+
: busyOperationLabel && !displayedBranch
182+
? busyOperationLabel
183+
: (displayedBranch ?? "No branch");
184+
185+
const showSpinner =
186+
effectiveLoading || (isCloudMode && open && cloudBranchesFetchingMore);
187+
188+
const isDisabled = !!(disabled || !repoPath || localBusy);
189+
const disabledReason =
190+
localBusy && busyOperationLabel
191+
? `${busyOperationLabel} in progress — finish or abort it to switch branches.`
192+
: null;
193+
const inputValue = isCloudMode ? (cloudSearchQuery ?? "") : searchQuery;
194+
const trimmedInputValue = inputValue.trim();
195+
const canUseInputBranch =
196+
!isDisabled &&
197+
trimmedInputValue.length > 0 &&
198+
trimmedInputValue !== displayedBranch;
199+
const showUseInputBranchAction =
200+
isCloudMode &&
201+
canUseInputBranch &&
202+
!branches.some((branch) => branch === trimmedInputValue);
203+
167204
const handleBranchChange = (value: string | null) => {
168205
if (!value) return;
169206
if (value === CREATE_BRANCH_ACTION) {
@@ -175,12 +212,15 @@ export function BranchSelector({
175212
);
176213
return;
177214
}
215+
const branchName =
216+
value === USE_INPUT_BRANCH_ACTION ? trimmedInputValue : value;
217+
if (!branchName) return;
178218
if (isSelectionOnly) {
179-
onBranchSelect?.(value);
180-
} else if (value !== currentBranch) {
219+
onBranchSelect?.(branchName);
220+
} else if (branchName !== currentBranch) {
181221
checkoutMutation.mutate({
182222
directoryPath: repoPath as string,
183-
branchName: value,
223+
branchName,
184224
});
185225
}
186226
if (isCloudMode) {
@@ -198,49 +238,20 @@ export function BranchSelector({
198238
}
199239
};
200240

201-
// In local mode, surface in-progress git operations (rebase/merge/etc.) so the
202-
// user understands why there's no current branch and why we won't let them
203-
// checkout a different one — checkout would fail with a hard-to-read git error.
204-
const localBusy = !isSelectionOnly && busyState?.busy === true;
205-
const busyOperationLabel =
206-
localBusy && busyState?.busy
207-
? BUSY_OPERATION_LABEL[busyState.operation]
208-
: null;
209-
210-
const displayText = effectiveLoading
211-
? "Loading..."
212-
: busyOperationLabel && !displayedBranch
213-
? busyOperationLabel
214-
: (displayedBranch ?? "No branch");
215-
216-
const showSpinner =
217-
effectiveLoading || (isCloudMode && open && cloudBranchesFetchingMore);
218-
219-
const isDisabled = !!(
220-
disabled ||
221-
!repoPath ||
222-
cloudStillLoading ||
223-
localBusy
224-
);
225-
const disabledReason =
226-
localBusy && busyOperationLabel
227-
? `${busyOperationLabel} in progress — finish or abort it to switch branches.`
228-
: null;
229-
const inputValue = isCloudMode ? (cloudSearchQuery ?? "") : searchQuery;
230-
const trimmedInputValue = inputValue.trim();
231-
const canUseInputBranch =
232-
!isDisabled &&
233-
trimmedInputValue.length > 0 &&
234-
trimmedInputValue !== displayedBranch;
235-
236241
const handleUseInputBranch = () => {
237242
if (!canUseInputBranch) return;
238243
handleBranchChange(trimmedInputValue);
239244
};
240245

246+
const comboboxItems = isCloudMode
247+
? showUseInputBranchAction
248+
? [USE_INPUT_BRANCH_ACTION, ...branches]
249+
: branches
250+
: [...branches, CREATE_BRANCH_ACTION];
251+
241252
return (
242253
<Combobox
243-
items={isCloudMode ? branches : [...branches, CREATE_BRANCH_ACTION]}
254+
items={comboboxItems}
244255
limit={COMBOBOX_LIMIT}
245256
autoHighlight
246257
value={displayedBranch}
@@ -378,19 +389,35 @@ export function BranchSelector({
378389
)}
379390

380391
<ComboboxList className="max-h-[min(14rem,calc(var(--available-height,14rem)-5rem))]">
381-
{(item: string) =>
382-
item === CREATE_BRANCH_ACTION ? (
383-
<ComboboxListFooter key="footer">
392+
{(item: string) => {
393+
if (item === CREATE_BRANCH_ACTION) {
394+
return (
395+
<ComboboxListFooter key="footer">
396+
<ComboboxItem
397+
value={CREATE_BRANCH_ACTION}
398+
title="Create new branch"
399+
className="text-accent-foreground"
400+
>
401+
<Plus size={11} weight="bold" />
402+
Create new branch
403+
</ComboboxItem>
404+
</ComboboxListFooter>
405+
);
406+
}
407+
if (item === USE_INPUT_BRANCH_ACTION) {
408+
return (
384409
<ComboboxItem
385-
value={CREATE_BRANCH_ACTION}
386-
title="Create new branch"
410+
key={USE_INPUT_BRANCH_ACTION}
411+
value={USE_INPUT_BRANCH_ACTION}
412+
title={`Use "${trimmedInputValue}" as branch name`}
387413
className="text-accent-foreground"
388414
>
389415
<Plus size={11} weight="bold" />
390-
Create new branch
416+
Use "{trimmedInputValue}" as branch name
391417
</ComboboxItem>
392-
</ComboboxListFooter>
393-
) : (
418+
);
419+
}
420+
return (
394421
<ComboboxItem
395422
key={item}
396423
value={item}
@@ -399,8 +426,8 @@ export function BranchSelector({
399426
>
400427
{item}
401428
</ComboboxItem>
402-
)
403-
}
429+
);
430+
}}
404431
</ComboboxList>
405432

406433
{isCloudMode && cloudBranchesHasMore ? (

apps/code/src/renderer/hooks/useIntegrations.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
useIntegrationSelectors,
66
useIntegrationStore,
77
} from "@features/integrations/stores/integrationStore";
8+
import { useDebounce } from "@hooks/useDebounce";
89
import type { UserGitHubIntegration } from "@renderer/api/posthogClient";
910
import { useQueries, useQueryClient } from "@tanstack/react-query";
1011
import {
@@ -17,6 +18,12 @@ import {
1718
import { useAuthenticatedInfiniteQuery } from "./useAuthenticatedInfiniteQuery";
1819
import { useAuthenticatedQuery } from "./useAuthenticatedQuery";
1920

21+
// Branch search hits a slow remote endpoint (GitHub via PostHog proxy). Debounce
22+
// keystrokes so we fire at most one request per typing burst. Empty searches
23+
// skip the debounce so closing the picker (which resets search to "") clears
24+
// stale results immediately.
25+
const BRANCH_SEARCH_DEBOUNCE_MS = 300;
26+
2027
const integrationKeys = {
2128
all: ["integrations"] as const,
2229
list: () => [...integrationKeys.all, "list"] as const,
@@ -366,11 +373,15 @@ export function useGithubBranches(
366373
search?: string,
367374
enabled: boolean = true,
368375
) {
369-
const deferredSearch = useDeferredValue(search?.trim() ?? "");
376+
const trimmedSearch = search?.trim() ?? "";
377+
const debouncedSearch = useDebounce(
378+
trimmedSearch,
379+
trimmedSearch ? BRANCH_SEARCH_DEBOUNCE_MS : 0,
380+
);
370381
const queryEnabled = enabled && !!integrationId && !!repo;
371382

372383
const query = useAuthenticatedInfiniteQuery<GithubBranchesPage, number>(
373-
integrationKeys.branches(integrationId, repo, deferredSearch),
384+
integrationKeys.branches(integrationId, repo, debouncedSearch),
374385
async (client, offset) => {
375386
if (!integrationId || !repo) {
376387
return { branches: [], defaultBranch: null, hasMore: false };
@@ -382,7 +393,7 @@ export function useGithubBranches(
382393
repo,
383394
offset,
384395
pageSize,
385-
deferredSearch,
396+
debouncedSearch,
386397
);
387398
},
388399
{
@@ -435,11 +446,15 @@ export function useUserGithubBranches(
435446
search?: string,
436447
enabled: boolean = true,
437448
) {
438-
const deferredSearch = useDeferredValue(search?.trim() ?? "");
449+
const trimmedSearch = search?.trim() ?? "";
450+
const debouncedSearch = useDebounce(
451+
trimmedSearch,
452+
trimmedSearch ? BRANCH_SEARCH_DEBOUNCE_MS : 0,
453+
);
439454
const queryEnabled = enabled && !!installationId && !!repo;
440455

441456
const query = useAuthenticatedInfiniteQuery<GithubBranchesPage, number>(
442-
userGithubIntegrationKeys.branches(installationId, repo, deferredSearch),
457+
userGithubIntegrationKeys.branches(installationId, repo, debouncedSearch),
443458
async (client, offset) => {
444459
if (!installationId || !repo) {
445460
return { branches: [], defaultBranch: null, hasMore: false };
@@ -451,7 +466,7 @@ export function useUserGithubBranches(
451466
repo,
452467
offset,
453468
pageSize,
454-
deferredSearch,
469+
debouncedSearch,
455470
);
456471
},
457472
{

0 commit comments

Comments
 (0)