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:
Bohdan Triapitsyn
2026-08-30 14:17:12 +03:00
parent 9847ef179f
commit 65054a5f4a
20 changed files with 461 additions and 272 deletions
@@ -20,6 +20,7 @@ import { useDeviceInfo } from '@/lib/device';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { SimpleMarkdownRenderer } from '@/components/chat/MarkdownRenderer';
import { Icon } from "@/components/icon/Icon";
import { GitHubAccountControl } from '@/components/github/GitHubAccountControl';
import { useUIStore } from '@/stores/useUIStore';
import { useWalkthroughStore } from '@/stores/useWalkthroughStore';
import { WALKTHROUGH_ACTION_CLASS } from '@/components/views/walkthrough/walkthroughAction';
@@ -102,6 +103,10 @@ const getPrVisualState = (status: GitHubPullRequestStatus | null): 'draft' | 'op
};
const PR_ACTION_REFRESH_DELAYS_MS = [2_000, 5_000] as const;
// A manual refresh keeps its spinner visible at least this long: the request
// often answers from the server cache within a few milliseconds, and a
// spinner that never reaches the screen reads as "the button did nothing".
const PR_MANUAL_REFRESH_MIN_SPIN_MS = 600;
const branchToTitle = (branch: string): string => {
return branch
@@ -337,7 +342,7 @@ export const PullRequestSection: React.FC<{
const showWalkthroughAction = !isMobile && screenWidth >= 768 && !isVSCodeRuntime();
const openGitHubSettings = React.useCallback(() => {
setSettingsPage('github');
setSettingsPage('integrations');
setSettingsDialogOpen(true);
}, [setSettingsDialogOpen, setSettingsPage]);
@@ -1040,6 +1045,31 @@ export const PullRequestSection: React.FC<{
await refreshPrStatus(prStatusKey, options);
}, [prStatusKey, refreshPrStatus]);
const [isManualRefreshing, setIsManualRefreshing] = React.useState(false);
const manualRefreshMountedRef = React.useRef(true);
React.useEffect(() => {
manualRefreshMountedRef.current = true;
return () => {
manualRefreshMountedRef.current = false;
};
}, []);
const refreshManually = React.useCallback(async () => {
if (isManualRefreshing) return;
setIsManualRefreshing(true);
const startedAt = Date.now();
try {
await refresh({ force: true });
} finally {
const remaining = PR_MANUAL_REFRESH_MIN_SPIN_MS - (Date.now() - startedAt);
if (remaining > 0) {
await new Promise((resolve) => window.setTimeout(resolve, remaining));
}
if (manualRefreshMountedRef.current) {
setIsManualRefreshing(false);
}
}
}, [isManualRefreshing, refresh]);
const scheduleActionRefresh = React.useCallback(() => {
pendingActionRefreshTimersRef.current.forEach((timerId) => {
window.clearTimeout(timerId);
@@ -1406,7 +1436,10 @@ export const PullRequestSection: React.FC<{
return (
<section className="border-0 bg-transparent rounded-none">
<div className="space-y-1 pt-3">
<div className="typography-ui-header font-semibold text-foreground">{t('gitView.pullRequest.title')}</div>
<div className="flex items-center justify-between gap-2">
<div className="typography-ui-header font-semibold text-foreground">{t('gitView.pullRequest.title')}</div>
<GitHubAccountControl />
</div>
<div className="typography-micro text-muted-foreground">
{t('gitView.pullRequest.availableOnFeatureBranches')}
</div>
@@ -1450,7 +1483,7 @@ export const PullRequestSection: React.FC<{
return (
<section className={containerClassName}>
<div className={headerClassName}>
<div className="flex items-start justify-between gap-2">
<div className="@container/pr-actions flex items-start justify-between gap-2">
<div className="flex min-w-0 items-center gap-2">
{pr ? (
<Button
@@ -1472,27 +1505,47 @@ export const PullRequestSection: React.FC<{
<span className="typography-meta text-muted-foreground truncate">#{pr.number}</span>
) : null}
</div>
<div className="flex shrink-0 items-center gap-1">
{isLoading ? <Icon name="loader-4" className="size-4 animate-spin text-muted-foreground" /> : null}
<div className="flex shrink-0 items-center gap-1.5">
{pr && showWalkthroughAction ? (
<Button
variant="outline"
size="sm"
className={cn('pr-actions__walkthrough-button h-7 shrink-0 gap-1.5 px-2', WALKTHROUGH_ACTION_CLASS)}
onClick={() => {
requestWalkthroughSource(directory, { kind: 'pr', number: pr.number });
openContextSurface(directory, 'walkthrough');
}}
aria-label={t('walkthrough.action.open')}
>
<Icon name="route" className="size-4" />
<span className="pr-actions__walkthrough-label typography-ui-label">
{t('walkthrough.action.open')}
</span>
</Button>
) : null}
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="inline-flex size-5 items-center justify-center rounded hover:bg-interactive-hover/60 disabled:opacity-40"
disabled={isLoading}
onClick={() => void refresh({ force: true })}
<Button
variant="ghost"
size="sm"
className="h-7 w-7 px-0"
disabled={isLoading || isManualRefreshing}
onClick={() => void refreshManually()}
aria-label={t('gitView.pr.actions.refreshAria')}
>
<Icon name="refresh" className="size-3.5 text-muted-foreground" />
</button>
{isLoading || isManualRefreshing
? <Icon name="loader-4" className="size-4 animate-spin text-muted-foreground" />
: <Icon name="refresh" className="size-4 text-muted-foreground" />}
</Button>
</TooltipTrigger>
<TooltipContent><p>{t('gitView.pr.actions.refresh')}</p></TooltipContent>
</Tooltip>
<GitHubAccountControl className="h-7 w-7" />
</div>
</div>
{pr ? (
<div className="@container/pr-actions flex min-w-0 items-center justify-between gap-2">
<div className="flex min-w-0 items-center justify-between gap-2">
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 typography-micro text-muted-foreground">
<span style={{ color: prColorVar }}>{prStatusText}</span>
{checks ? (
@@ -1508,23 +1561,6 @@ export const PullRequestSection: React.FC<{
) : null}
</div>
<div className="flex shrink-0 items-center gap-1.5">
{showWalkthroughAction ? (
<Button
variant="outline"
size="sm"
className={cn('pr-actions__walkthrough-button h-7 shrink-0 gap-1.5 px-2', WALKTHROUGH_ACTION_CLASS)}
onClick={() => {
requestWalkthroughSource(directory, { kind: 'pr', number: pr.number });
openContextSurface(directory, 'walkthrough');
}}
aria-label={t('walkthrough.action.open')}
>
<Icon name="route" className="size-4" />
<span className="pr-actions__walkthrough-label typography-ui-label">
{t('walkthrough.action.open')}
</span>
</Button>
) : null}
{canMerge && pr.draft && pr.state === 'open' ? (
<Tooltip>
<TooltipTrigger asChild>