feat(ui): gate the pull-request surface on GitHub, move the account into it, and GitHub sign-in into Integrations
The pull-request rail icon now appears only while GitHub is connected (OAuth or gh CLI), like Linear; Linear sits after the walkthrough in the default rail order. The GitHub account avatar and switcher leave the header for the pull-request panel, where the walkthrough, refresh, and account controls share one row and one height, and the account stays visible on the panel's empty state. A manual refresh keeps its spinner on screen long enough to read as work done. GitHub sign-in moves from Settings → Git to Settings → Integrations → Built-in integrations as a card before Linear; search and the connect buttons follow it.
This commit is contained in:
@@ -9,7 +9,6 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
@@ -30,7 +29,6 @@ import { useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore';
|
||||
import { streamPerfCount } from '@/stores/utils/streamDebug';
|
||||
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
|
||||
|
||||
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import { useDesktopWindowControlsLayout } from '@/hooks/useDesktopWindowControlsLayout';
|
||||
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
|
||||
@@ -45,7 +43,6 @@ import {
|
||||
|
||||
import {
|
||||
} from '@/components/ui/collapsible';
|
||||
import type { GitHubAuthStatus } from '@/lib/api/types';
|
||||
import type { SessionContextUsage } from '@/stores/types/sessionTypes';
|
||||
import { DesktopHostSwitcherDialog } from '@/components/desktop/DesktopHostSwitcher';
|
||||
import { OpenInAppButton } from '@/components/desktop/OpenInAppButton';
|
||||
@@ -123,132 +120,6 @@ const HeaderIconActionButton = React.memo(function HeaderIconActionButton({
|
||||
);
|
||||
});
|
||||
|
||||
type DesktopGitHubControlProps = {
|
||||
isMobile: boolean;
|
||||
githubAuthStatus: GitHubAuthStatus | null;
|
||||
githubAccounts: Array<NonNullable<GitHubAuthStatus['accounts']>[number]>;
|
||||
githubAvatarUrl: string | null;
|
||||
githubLogin: string | null;
|
||||
isSwitchingGitHubAccount: boolean;
|
||||
handleGitHubAccountSwitch: (accountId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
const DesktopGitHubControl = React.memo(function DesktopGitHubControl({
|
||||
isMobile,
|
||||
githubAuthStatus,
|
||||
githubAccounts,
|
||||
githubAvatarUrl,
|
||||
githubLogin,
|
||||
isSwitchingGitHubAccount,
|
||||
handleGitHubAccountSwitch,
|
||||
}: DesktopGitHubControlProps) {
|
||||
const { t } = useI18n();
|
||||
if (!githubAuthStatus?.connected || isMobile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (githubAccounts.length > 1) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
DESKTOP_HEADER_ICON_BUTTON_CLASS,
|
||||
'h-7 w-7 overflow-hidden rounded-full border border-border/60 bg-muted/80 p-0'
|
||||
)}
|
||||
title={githubLogin ? t('header.github.connectedWithLogin', { login: githubLogin }) : t('header.github.connected')}
|
||||
disabled={isSwitchingGitHubAccount}
|
||||
>
|
||||
{githubAvatarUrl ? (
|
||||
<img
|
||||
src={githubAvatarUrl}
|
||||
alt={githubLogin ? t('header.github.avatarWithLogin', { login: githubLogin }) : t('header.github.avatar')}
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
) : (
|
||||
<Icon name="github-fill" className="h-3.5 w-3.5 text-foreground" />
|
||||
)}
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-64">
|
||||
<DropdownMenuLabel className="typography-ui-header font-semibold text-foreground">
|
||||
{t('header.github.accountsTitle')}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{githubAccounts.map((account) => {
|
||||
const accountUser = account.user;
|
||||
const isCurrent = Boolean(account.current);
|
||||
const sourceLabel = account.source === 'gh-cli'
|
||||
? t('header.github.accountSource.cli')
|
||||
: t('header.github.accountSource.oauth');
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={account.id}
|
||||
className="gap-2"
|
||||
disabled={isSwitchingGitHubAccount}
|
||||
onSelect={() => {
|
||||
if (!isCurrent) {
|
||||
void handleGitHubAccountSwitch(account.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{accountUser?.avatarUrl ? (
|
||||
<img
|
||||
src={accountUser.avatarUrl}
|
||||
alt={accountUser.login ? t('header.github.avatarWithLogin', { login: accountUser.login }) : t('header.github.avatar')}
|
||||
className="h-6 w-6 rounded-full border border-border/60 bg-muted object-cover"
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full border border-border/60 bg-muted">
|
||||
<Icon name="github-fill" className="h-3 w-3 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
<span className="flex min-w-0 flex-1 flex-col">
|
||||
<span className="truncate typography-ui-label text-foreground">
|
||||
{accountUser?.name?.trim() || accountUser?.login || 'GitHub'}
|
||||
</span>
|
||||
{accountUser?.login ? (
|
||||
<span className="truncate typography-micro text-muted-foreground">
|
||||
<span className="font-mono">{accountUser.login}</span>
|
||||
<span className="mx-1 opacity-50">·</span>
|
||||
<span>{sourceLabel}</span>
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
{isCurrent ? <Icon name="check" className="h-4 w-4 text-primary" /> : null}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="app-region-no-drag flex h-7 w-7 items-center justify-center overflow-hidden rounded-full border border-border/60 bg-muted/80"
|
||||
title={githubLogin ? t('header.github.connectedWithLogin', { login: githubLogin }) : t('header.github.connected')}
|
||||
>
|
||||
{githubAvatarUrl ? (
|
||||
<img
|
||||
src={githubAvatarUrl}
|
||||
alt={githubLogin ? t('header.github.avatarWithLogin', { login: githubLogin }) : t('header.github.avatar')}
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
) : (
|
||||
<Icon name="github-fill" className="h-3.5 w-3.5 text-foreground" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
type DesktopServicesMenuProps = {
|
||||
isDesktopApp: boolean;
|
||||
currentInstanceLabel: string;
|
||||
@@ -439,7 +310,6 @@ export const Header: React.FC = () => {
|
||||
const sessionTabsEnabled = useUIStore((state) => state.sessionTabsEnabled);
|
||||
|
||||
const getCurrentModel = useConfigStore((state) => state.getCurrentModel);
|
||||
const runtimeApis = useRuntimeAPIs();
|
||||
|
||||
const getContextUsage = useSessionUIStore((state) => state.getContextUsage);
|
||||
const isNewSessionDraftOpen = useSessionUIStore((state) => Boolean(state.newSessionDraft?.open));
|
||||
@@ -488,8 +358,6 @@ export const Header: React.FC = () => {
|
||||
const loadQuotaSettings = useQuotaStore((state) => state.loadSettings);
|
||||
|
||||
const { isMobile } = useDeviceInfo();
|
||||
const githubAuthStatus = useGitHubAuthStore((state) => state.status);
|
||||
const setGitHubAuthStatus = useGitHubAuthStore((state) => state.setStatus);
|
||||
|
||||
const headerRef = React.useRef<HTMLElement | null>(null);
|
||||
|
||||
@@ -571,10 +439,6 @@ export const Header: React.FC = () => {
|
||||
}
|
||||
}, [contextUsage, currentSessionId, isContextUsageResolvedForSession]);
|
||||
|
||||
const githubAvatarUrl = githubAuthStatus?.connected ? (githubAuthStatus.user?.avatarUrl ?? null) : null;
|
||||
const githubLogin = githubAuthStatus?.connected ? (githubAuthStatus.user?.login ?? null) : null;
|
||||
const githubAccounts = githubAuthStatus?.accounts ?? [];
|
||||
const [isSwitchingGitHubAccount, setIsSwitchingGitHubAccount] = React.useState(false);
|
||||
const [isDesktopServicesOpen, setIsDesktopServicesOpen] = React.useState(false);
|
||||
const [currentInstanceLabel, setCurrentInstanceLabel] = React.useState('Local');
|
||||
const [currentInstanceIsLocal, setCurrentInstanceIsLocal] = React.useState(true);
|
||||
@@ -1183,37 +1047,6 @@ export const Header: React.FC = () => {
|
||||
sessionDirectory,
|
||||
]);
|
||||
|
||||
const handleGitHubAccountSwitch = React.useCallback(async (accountId: string) => {
|
||||
if (!accountId || isSwitchingGitHubAccount) return;
|
||||
setIsSwitchingGitHubAccount(true);
|
||||
try {
|
||||
const payload = runtimeApis.github
|
||||
? await runtimeApis.github.authActivate(accountId)
|
||||
: await (async () => {
|
||||
const response = await runtimeFetch('/api/github/auth/activate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ accountId }),
|
||||
});
|
||||
const body = (await response.json().catch(() => null)) as
|
||||
| (GitHubAuthStatus & { error?: string })
|
||||
| null;
|
||||
if (!response.ok || !body) {
|
||||
throw new Error(body?.error || response.statusText);
|
||||
}
|
||||
return body;
|
||||
})();
|
||||
|
||||
setGitHubAuthStatus(payload);
|
||||
} catch (error) {
|
||||
console.error('Failed to switch GitHub account:', error);
|
||||
} finally {
|
||||
setIsSwitchingGitHubAccount(false);
|
||||
}
|
||||
}, [isSwitchingGitHubAccount, runtimeApis.github, setGitHubAuthStatus]);
|
||||
|
||||
|
||||
|
||||
@@ -1482,15 +1315,6 @@ export const Header: React.FC = () => {
|
||||
onOpenRemoteUpdate={openRemoteInstanceUpdate}
|
||||
/>
|
||||
) : null}
|
||||
<DesktopGitHubControl
|
||||
isMobile={isMobile}
|
||||
githubAuthStatus={githubAuthStatus}
|
||||
githubAccounts={githubAccounts}
|
||||
githubAvatarUrl={githubAvatarUrl}
|
||||
githubLogin={githubLogin}
|
||||
isSwitchingGitHubAccount={isSwitchingGitHubAccount}
|
||||
handleGitHubAccountSwitch={handleGitHubAccountSwitch}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user