fix: Improve account selector (#626)

* fix: Improve account selector

* cr

* cr

* cr
This commit is contained in:
Brace Sproul 2025-07-31 12:25:24 -07:00 • committed by GitHub
parent 9906dd4e2a
commit bedd2c9b68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 198 additions and 47 deletions

View file

@ -16,6 +16,7 @@ import { MANAGER_GRAPH_ID } from "@open-swe/shared/constants";
import { useThreadsStatus } from "@/hooks/useThreadsStatus";
import { cn } from "@/lib/utils";
import { threadsToMetadata } from "@/lib/thread-utils";
import { UserPopover } from "@/components/user-popover";
import { OpenSWELogoSVG } from "@/components/icons/openswe";
type FilterStatus =
@ -122,8 +123,8 @@ function AllThreadsPageContent() {
</span>
</div>
<div className="flex items-center gap-2">
<InstallationSelector />
<ThemeToggle />
<UserPopover />
</div>
</div>
</div>

View file

@ -1,42 +0,0 @@
"use client";
import { useState } from "react";
import { LogOut } from "lucide-react";
import { TooltipIconButton } from "../ui/tooltip-icon-button";
const GITHUB_APP_INSTALLED_KEY = "github_app_installed";
export function GitHubLogoutButton() {
const [isLoading, setIsLoading] = useState(false);
const handleLogout = async () => {
setIsLoading(true);
try {
const response = await fetch("/api/auth/logout", {
method: "POST",
});
if (response.ok) {
localStorage.removeItem(GITHUB_APP_INSTALLED_KEY);
window.location.href = "/";
} else {
console.error("Logout failed");
}
} catch (error) {
console.error("Error during logout:", error);
} finally {
setIsLoading(false);
}
};
return (
<TooltipIconButton
tooltip="Logout"
onClick={handleLogout}
disabled={isLoading}
className="text-xs"
size="sm"
>
<LogOut className="size-4" />
</TooltipIconButton>
);
}

View file

@ -0,0 +1,194 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { useGitHubAppProvider } from "@/providers/GitHubApp";
import { Building2, LogOut, User } from "lucide-react";
import { GitHubSVG } from "@/components/icons/github";
import { cn } from "@/lib/utils";
import { useState } from "react";
interface UserPopoverProps {
className?: string;
}
export function UserPopover({ className }: UserPopoverProps) {
const {
installations,
currentInstallation,
installationsLoading: isLoading,
installationsError: error,
switchInstallation,
} = useGitHubAppProvider();
const [isLoggingOut, setIsLoggingOut] = useState(false);
const GITHUB_APP_INSTALLED_KEY = "github_app_installed";
const handleLogout = async () => {
setIsLoggingOut(true);
try {
const response = await fetch("/api/auth/logout", {
method: "POST",
});
if (response.ok) {
localStorage.removeItem(GITHUB_APP_INSTALLED_KEY);
window.location.href = "/";
} else {
console.error("Logout failed");
}
} catch (error) {
console.error("Error during logout:", error);
} finally {
setIsLoggingOut(false);
}
};
const handleValueChange = async (value: string) => {
await switchInstallation(value);
};
const getAccountIcon = (accountType: "User" | "Organization") => {
return accountType === "Organization" ? (
<Building2 className="h-4 w-4" />
) : (
<User className="h-4 w-4" />
);
};
if (isLoading || !currentInstallation) {
return (
<Button
variant="ghost"
size="sm"
disabled
className={cn("h-8 w-8 rounded-full p-0", className)}
>
<div className="bg-muted h-6 w-6 animate-pulse rounded-full" />
</Button>
);
}
if (error) {
return (
<Button
variant="ghost"
size="sm"
disabled
className={cn("h-8 w-8 rounded-full p-0", className)}
>
<div className="bg-destructive/20 flex h-6 w-6 items-center justify-center rounded-full">
<GitHubSVG className="text-destructive h-3 w-3" />
</div>
</Button>
);
}
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="sm"
className={cn("hover:bg-accent h-8 w-8 rounded-full p-0", className)}
>
<img
src={currentInstallation.avatarUrl}
alt={`${currentInstallation.accountName} avatar`}
className="h-6 w-6 rounded-full"
/>
</Button>
</PopoverTrigger>
<PopoverContent
className="w-80 p-0"
align="end"
>
<div className="p-4">
<div className="mb-4 flex items-center gap-3">
<img
src={currentInstallation.avatarUrl}
alt={`${currentInstallation.accountName} avatar`}
className="h-10 w-10 rounded-full"
/>
<div className="min-w-0 flex-1">
<div className="truncate font-medium">
{currentInstallation.accountName}
</div>
<div className="text-muted-foreground flex items-center gap-1 text-sm">
{getAccountIcon(currentInstallation.accountType)}
<span className="capitalize">
{currentInstallation.accountType.toLowerCase()}
</span>
</div>
</div>
</div>
{installations.length > 1 && (
<>
<div className="mb-4 space-y-2">
<label className="text-sm font-medium">Switch Account</label>
<Select
value={currentInstallation.id.toString()}
onValueChange={handleValueChange}
>
<SelectTrigger className="w-full">
<div className="flex flex-1 items-center gap-2">
<img
src={currentInstallation.avatarUrl}
alt={`${currentInstallation.accountName} avatar`}
className="h-4 w-4 rounded-full"
/>
<span className="truncate">
{currentInstallation.accountName}
</span>
</div>
</SelectTrigger>
<SelectContent>
{installations.map((installation) => (
<SelectItem
key={installation.id}
value={installation.id.toString()}
>
<div className="flex items-center gap-2">
<img
src={installation.avatarUrl}
alt={`${installation.accountName} avatar`}
className="h-4 w-4 rounded-full"
/>
<span>{installation.accountName}</span>
{getAccountIcon(installation.accountType)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Separator className="mb-4" />
</>
)}
<Button
variant="ghost"
className="w-full justify-start text-red-600 hover:bg-red-50 hover:text-red-700 dark:text-red-400 dark:hover:bg-red-950/50 dark:hover:text-red-300"
onClick={handleLogout}
disabled={isLoggingOut}
>
<LogOut className="mr-2 h-4 w-4" />
{isLoggingOut ? "Signing out..." : "Sign out"}
</Button>
</div>
</PopoverContent>
</Popover>
);
}

View file

@ -19,10 +19,9 @@ import { GitHubInstallationBanner } from "../github/installation-banner";
import { ApiKeyBanner } from "../api-key-banner";
import { QuickActions } from "./quick-actions";
import { DraftsSection } from "./drafts-section";
import { GitHubLogoutButton } from "../github/github-oauth-button";
import { MANAGER_GRAPH_ID } from "@open-swe/shared/constants";
import { TooltipIconButton } from "../ui/tooltip-icon-button";
import { InstallationSelector } from "../github/installation-selector";
import { UserPopover } from "../user-popover";
import { useThreadsStatus } from "@/hooks/useThreadsStatus";
import { Thread } from "@langchain/langgraph-sdk";
@ -126,7 +125,6 @@ export function DefaultView({ threads, threadsLoading }: DefaultViewProps) {
/>
</div>
<div className="flex items-center gap-4">
<InstallationSelector />
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-xs">ready</span>
<div className="h-1 w-1 rounded-full bg-green-500 dark:bg-green-600"></div>
@ -134,7 +132,7 @@ export function DefaultView({ threads, threadsLoading }: DefaultViewProps) {
<OpenDocumentationButton />
<OpenSettingsButton />
<ThemeToggle />
<GitHubLogoutButton />
<UserPopover />
</div>
</div>
</div>