Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
import { Button } from "@components/ui/Button";
import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient";
import { useAuthStateValue } from "@features/auth/hooks/authQueries";
import { useAuthenticatedQuery } from "@hooks/useAuthenticatedQuery";
import { useConnectUserGithub } from "@hooks/useConnectUserGithub";
import { useUserRepositoryIntegration } from "@hooks/useIntegrations";
import {
ArrowSquareOutIcon,
GithubLogoIcon,
InfoIcon,
} from "@phosphor-icons/react";
import { Spinner } from "@radix-ui/themes";
import { trpcClient } from "@renderer/trpc/client";
import { queryClient } from "@utils/queryClient";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";

async function openUrlInBrowser(url: string): Promise<void> {
try {
await trpcClient.os.openExternal.mutate({ url });
} catch {
window.open(url, "_blank", "noopener,noreferrer");
}
}

export function GitHubConnectionBanner() {
const { data: githubLogin, isLoading: loginLoading } = useAuthenticatedQuery(
Expand All @@ -30,33 +18,12 @@ export function GitHubConnectionBanner() {
);
const { hasGithubIntegration: hasGithubForProject } =
useUserRepositoryIntegration();
const apiClient = useOptionalAuthenticatedClient();
const projectId = useAuthStateValue((s) => s.projectId);
const cloudRegion = useAuthStateValue((s) => s.cloudRegion);
const awaitingLink = useRef(false);
const connectInFlight = useRef(false);
const [connecting, setConnecting] = useState(false);

const canConnectCloud =
apiClient != null && projectId != null && cloudRegion != null;

// After the user clicks connect and returns to the app, refetch to pick up the new github_login
useEffect(() => {
const onFocus = () => {
if (awaitingLink.current) {
awaitingLink.current = false;
void queryClient.invalidateQueries({ queryKey: ["github_login"] });
void queryClient.invalidateQueries({
queryKey: ["integrations", "list"],
});
void queryClient.invalidateQueries({
queryKey: ["user-github-integrations"],
});
}
};
window.addEventListener("focus", onFocus);
return () => window.removeEventListener("focus", onFocus);
}, []);
const {
connect,
isConnecting: connecting,
canConnect: canConnectCloud,
} = useConnectUserGithub();

if (loginLoading) {
return null;
Expand Down Expand Up @@ -109,37 +76,7 @@ export function GitHubConnectionBanner() {
</>
}
onClick={() => {
if (!canConnectCloud || connectInFlight.current) {
return;
}
connectInFlight.current = true;
awaitingLink.current = true;
setConnecting(true);
void (async () => {
try {
const res =
await apiClient.startGithubUserIntegrationConnect(projectId);
const installUrl = res.install_url?.trim() ?? "";
if (!installUrl) {
awaitingLink.current = false;
toast.error(
"GitHub connection did not return a URL. Please try again.",
);
return;
}
await openUrlInBrowser(installUrl);
} catch (e) {
awaitingLink.current = false;
toast.error(
e instanceof Error
? e.message
: "Failed to start GitHub connection",
);
} finally {
connectInFlight.current = false;
setConnecting(false);
}
})();
void connect();
}}
>
{connecting ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useConnectUserGithub } from "@hooks/useConnectUserGithub";
import { ArrowSquareOut, Info } from "@phosphor-icons/react";
import { Button, Callout, Flex, Spinner, Text } from "@radix-ui/themes";

export function CloudRepoFallbackNotice() {
const { connect, isConnecting, canConnect } = useConnectUserGithub();

return (
<Callout.Root color="amber" variant="soft" size="1">
<Flex align="center" gap="2" justify="between" wrap="wrap">
<Flex align="start" gap="2" className="min-w-0 flex-1">
<Callout.Icon>
<Info size={14} />
</Callout.Icon>
<Callout.Text>
<Text size="1">
Using your team's GitHub integration. Link your personal GitHub so
cloud PRs are authored as you.
</Text>
</Callout.Text>
</Flex>
<Button
size="1"
variant="soft"
color="amber"
disabled={!canConnect || isConnecting}
onClick={() => {
void connect();
}}
>
{isConnecting ? <Spinner size="1" /> : <ArrowSquareOut size={12} />}
{isConnecting ? "Waiting…" : "Connect GitHub"}
</Button>
</Flex>
</Callout.Root>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ import { useSettingsStore } from "@features/settings/stores/settingsStore";
import { useAutoFocusOnTyping } from "@hooks/useAutoFocusOnTyping";
import { useConnectivity } from "@hooks/useConnectivity";
import {
useUserGithubBranches,
useUserGithubRepositories,
useUserRepositoryIntegration,
useCloudGithubBranches,
useCloudGithubRepositories,
useCloudRepositoryIntegration,
} from "@hooks/useIntegrations";
import { X } from "@phosphor-icons/react";
import { ButtonGroup } from "@posthog/quill";
Expand All @@ -45,6 +45,7 @@ import { FOCUSABLE_SELECTOR } from "@utils/overlay";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { usePreviewConfig } from "../hooks/usePreviewConfig";
import { useTaskCreation } from "../hooks/useTaskCreation";
import { CloudRepoFallbackNotice } from "./CloudRepoFallbackNotice";
import { type WorkspaceMode, WorkspaceModeSelect } from "./WorkspaceModeSelect";

interface TaskInputProps {
Expand Down Expand Up @@ -175,26 +176,36 @@ export function TaskInput({
const setAdapter = (newAdapter: AgentAdapter) =>
setLastUsedAdapter(newAdapter);

const [selectedRepository, setSelectedRepository] = useState<string | null>(
() =>
initialCloudRepository?.toLowerCase() ??
lastUsedCloudRepository?.toLowerCase() ??
null,
);

const cloudRepoIntegration =
useCloudRepositoryIntegration(selectedRepository);
const {
source: cloudRepoSource,
repositories,
getInstallationIdForRepo,
getUserIntegrationIdForRepo,
isLoadingRepos,
isRefreshingRepos,
refreshRepositories,
} = useUserRepositoryIntegration();
githubIntegrationId: selectedGithubIntegrationId,
githubUserIntegrationId: selectedGithubUserIntegrationId,
} = cloudRepoIntegration;

const {
repositories: visibleCloudRepositories,
isPending: cloudRepositoriesLoading,
hasMore: cloudRepositoriesHasMore,
loadMore: loadMoreCloudRepositories,
} = useUserGithubRepositories(cloudRepoSearchQuery, isCloudRepoPickerOpen);
const [selectedRepository, setSelectedRepository] = useState<string | null>(
() =>
initialCloudRepository?.toLowerCase() ??
lastUsedCloudRepository?.toLowerCase() ??
null,
} = useCloudGithubRepositories(
cloudRepoSource,
cloudRepoSearchQuery,
isCloudRepoPickerOpen,
);

const selectedCloudRepository = useMemo(() => {
if (!selectedRepository) return null;
const lower = selectedRepository.toLowerCase();
Expand All @@ -203,13 +214,6 @@ export function TaskInput({
const { currentBranch, branchLoading, defaultBranch } =
useGitQueries(selectedDirectory);

const selectedGithubUserIntegrationId = selectedCloudRepository
? getUserIntegrationIdForRepo(selectedCloudRepository)
: undefined;
const selectedInstallationId = selectedCloudRepository
? getInstallationIdForRepo(selectedCloudRepository)
: undefined;

const {
data: cloudBranchData,
isPending: cloudBranchesLoading,
Expand All @@ -218,8 +222,9 @@ export function TaskInput({
hasMore: cloudBranchesHasMore,
loadMore: loadMoreCloudBranches,
refresh: refreshCloudBranches,
} = useUserGithubBranches(
selectedInstallationId,
} = useCloudGithubBranches(
cloudRepoSource,
cloudRepoIntegration,
selectedCloudRepository,
cloudBranchSearchQuery,
isCloudBranchPickerOpen,
Expand Down Expand Up @@ -432,6 +437,7 @@ export function TaskInput({
editorRef,
selectedDirectory,
selectedRepository: selectedCloudRepository,
githubIntegrationId: selectedGithubIntegrationId,
githubUserIntegrationId: selectedGithubUserIntegrationId,
workspaceMode: effectiveWorkspaceMode,
branch: branchForTaskCreation,
Expand Down Expand Up @@ -775,6 +781,10 @@ export function TaskInput({
</div>
)}
</Flex>

{workspaceMode === "cloud" &&
cloudRepoSource === "team" &&
!isLoadingRepos && <CloudRepoFallbackNotice />}
</Flex>
</Flex>

Expand Down
79 changes: 79 additions & 0 deletions apps/code/src/renderer/hooks/useConnectUserGithub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient";
import { useAuthStateValue } from "@features/auth/hooks/authQueries";
import { trpcClient } from "@renderer/trpc/client";
import { toast } from "@renderer/utils/toast";
import { useQueryClient } from "@tanstack/react-query";
import { useCallback, useEffect, useRef, useState } from "react";

interface UseConnectUserGithubResult {
connect: () => Promise<void>;
isConnecting: boolean;
canConnect: boolean;
}

/**
* Starts the GitHub user-integration install flow and refreshes the relevant
* query caches once the user returns to the window. Shared between the inbox
* banner and the cloud-task fallback notice; onboarding has its own polling
* state machine and uses the underlying API directly.
*/
export function useConnectUserGithub(): UseConnectUserGithubResult {
const apiClient = useOptionalAuthenticatedClient();
const projectId = useAuthStateValue((s) => s.projectId);
const cloudRegion = useAuthStateValue((s) => s.cloudRegion);
const queryClient = useQueryClient();

const [isConnecting, setIsConnecting] = useState(false);
const awaitingLink = useRef(false);
const inFlight = useRef(false);

const canConnect =
apiClient != null && projectId != null && cloudRegion != null;

useEffect(() => {
const onFocus = () => {
if (!awaitingLink.current) return;
awaitingLink.current = false;
void queryClient.invalidateQueries({ queryKey: ["github_login"] });
void queryClient.invalidateQueries({
queryKey: ["integrations", "list"],
});
void queryClient.invalidateQueries({
queryKey: ["user-github-integrations"],
});
};
window.addEventListener("focus", onFocus);
return () => window.removeEventListener("focus", onFocus);
}, [queryClient]);

const connect = useCallback(async () => {
if (!canConnect || inFlight.current || !apiClient || !projectId) return;
inFlight.current = true;
awaitingLink.current = true;
setIsConnecting(true);
try {
const res = await apiClient.startGithubUserIntegrationConnect(projectId);
const installUrl = res.install_url?.trim() ?? "";
if (!installUrl) {
awaitingLink.current = false;
toast.error(
"GitHub connection did not return a URL. Please try again.",
);
return;
}
await trpcClient.os.openExternal.mutate({ url: installUrl });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 window.open fallback removed

The original openUrlInBrowser helper silently fell back to window.open when trpcClient.os.openExternal.mutate threw. The new code lets the throw propagate to the outer catch, so the user sees an error toast instead of the URL opening. In non-Electron environments (tests, browser builds) or if the IPC channel is unavailable, the install flow now silently fails rather than opening the tab. Consider restoring the fallback:

Suggested change
await trpcClient.os.openExternal.mutate({ url: installUrl });
try {
await trpcClient.os.openExternal.mutate({ url: installUrl });
} catch {
window.open(installUrl, "_blank", "noopener,noreferrer");
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/hooks/useConnectUserGithub.ts
Line: 64

Comment:
**`window.open` fallback removed**

The original `openUrlInBrowser` helper silently fell back to `window.open` when `trpcClient.os.openExternal.mutate` threw. The new code lets the throw propagate to the outer `catch`, so the user sees an error toast instead of the URL opening. In non-Electron environments (tests, browser builds) or if the IPC channel is unavailable, the install flow now silently fails rather than opening the tab. Consider restoring the fallback:

```suggestion
      try {
        await trpcClient.os.openExternal.mutate({ url: installUrl });
      } catch {
        window.open(installUrl, "_blank", "noopener,noreferrer");
      }
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mmm maybe we don;t need this

} catch (error) {
awaitingLink.current = false;
toast.error(
error instanceof Error
? error.message
: "Failed to start GitHub connection",
);
} finally {
inFlight.current = false;
setIsConnecting(false);
}
}, [apiClient, canConnect, projectId]);

return { connect, isConnecting, canConnect };
}
Loading
Loading