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
@@ -19,7 +19,6 @@ import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger }
import { useGitIdentitiesStore, type GitIdentityProfile, type DiscoveredGitCredential } from '@/stores/useGitIdentitiesStore';
import { useShallow } from 'zustand/react/shallow';
import { GitSettings } from '@/components/sections/openchamber/GitSettings';
import { GitHubSettings } from '@/components/sections/openchamber/GitHubSettings';
import { GitIdentityEditorDialog } from './GitIdentityEditorDialog';
import { Icon } from "@/components/icon/Icon";
import type { IconName } from "@/components/icon/icons";
@@ -121,10 +120,9 @@ export const GitPage: React.FC = () => {
title={t('settings.page.git.title')}
showSaveStatus
>
<GitHubSettings />
<SettingsSection
title={t('settings.gitIdentities.page.section.title')}
divider={false}
headerAction={(
<Button size="sm" variant="outline" onClick={() => openEditor('new')}>
<Icon name="add" className="w-3.5 h-3.5 mr-1" /> {t('settings.common.badge.new')}
@@ -0,0 +1,72 @@
import React from 'react';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { Icon } from '@/components/icon/Icon';
import { GitHubSettings } from '@/components/sections/openchamber/GitHubSettings';
import { useI18n } from '@/lib/i18n';
import { cn } from '@/lib/utils';
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
/**
* The GitHub row of Settings → Integrations → Built-in integrations: a
* collapsible card whose body is the account/device-flow UI. Sign-in status
* shows on the collapsed row so the page answers "am I connected?" at a
* glance, like the Linear card beside it.
*/
export const GitHubIntegration: React.FC = () => {
const { t } = useI18n();
const status = useGitHubAuthStore((state) => state.status);
const isLoading = useGitHubAuthStore((state) => state.isLoading);
const hasChecked = useGitHubAuthStore((state) => state.hasChecked);
const [open, setOpen] = React.useState(false);
const connected = status?.connected === true;
const statusLabel = isLoading && !hasChecked
? t('common.loading')
: connected
? (status?.user?.login?.trim() || t('settings.github.page.status.active'))
: t('settings.integrations.github.status.notConnected');
const statusClassName = connected
? 'bg-[var(--status-success)]/15 text-[var(--status-success)]'
: 'bg-[var(--surface-muted)] text-muted-foreground';
return (
<Collapsible open={open} onOpenChange={setOpen}>
<div
data-settings-item="integrations.github"
className="overflow-hidden rounded-xl border border-[var(--interactive-border)] bg-[var(--surface-elevated)]"
>
<CollapsibleTrigger
className="flex w-full min-w-0 items-center gap-3 px-4 py-3 text-left hover:bg-[var(--interactive-hover)]/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--interactive-focus-ring)]"
>
<div className="flex size-10 shrink-0 items-center justify-center rounded-[10px] bg-[var(--surface-muted)]">
<Icon name="github-fill" className="size-5 text-foreground" />
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-semibold text-foreground">
{t('settings.integrations.github.title')}
</div>
<p className="mt-0.5 line-clamp-1 text-xs leading-snug text-muted-foreground">
{t('settings.integrations.github.description')}
</p>
</div>
<span
aria-live="polite"
className={cn('max-w-36 shrink-0 truncate rounded-full px-2 py-0.5 text-[10px] font-medium', statusClassName)}
>
{statusLabel}
</span>
<Icon
name="arrow-down-s"
className={cn(
'size-4 shrink-0 text-muted-foreground transition-transform duration-150 ease-out motion-reduce:transition-none',
open && 'rotate-180',
)}
/>
</CollapsibleTrigger>
<CollapsibleContent className="border-t border-[var(--interactive-border)] px-4 py-4">
<GitHubSettings embedded />
</CollapsibleContent>
</div>
</Collapsible>
);
};
@@ -1,7 +1,10 @@
import React from 'react';
import { SettingsPageLayout } from '@/components/sections/shared/SettingsPageLayout';
import { SettingsSection } from '@/components/sections/shared/SettingsSection';
import { useI18n } from '@/lib/i18n';
import { isVSCodeRuntime } from '@/lib/desktop';
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
import { GitHubIntegration } from './GitHubIntegration';
import { LinearSettings } from './LinearSettings';
import { ThirdPartyIntegrationsSection } from './ThirdPartyIntegrationsSection';
@@ -15,7 +18,11 @@ export const IntegrationsPage: React.FC<IntegrationsPageProps> = ({
onOpenPluginManager,
}) => {
const { t } = useI18n();
// GitHub sign-in is an OpenChamber server feature; the VS Code extension
// uses the editor's own GitHub session instead.
const hasGitHub = !isVSCodeRuntime();
const hasLinear = Boolean(getRegisteredRuntimeAPIs()?.linear);
const hasBuiltIn = hasGitHub || hasLinear;
return (
<SettingsPageLayout
@@ -23,9 +30,20 @@ export const IntegrationsPage: React.FC<IntegrationsPageProps> = ({
description={t('settings.page.integrations.description')}
showSaveStatus
>
{hasLinear ? <LinearSettings /> : null}
{hasBuiltIn ? (
<SettingsSection
title={t('settings.integrations.firstParty.title')}
info={t('settings.integrations.firstParty.info')}
divider={false}
settingsItem="integrations.first-party"
contentClassName="space-y-3"
>
{hasGitHub ? <GitHubIntegration /> : null}
{hasLinear ? <LinearSettings /> : null}
</SettingsSection>
) : null}
<ThirdPartyIntegrationsSection
divider={hasLinear}
divider={hasBuiltIn}
onOpenProviderSetup={onOpenProviderSetup}
onOpenPluginManager={onOpenPluginManager}
/>
@@ -9,7 +9,6 @@ import { openExternalUrl } from '@/lib/url';
import { useI18n } from '@/lib/i18n';
import { focusDesktopWindow, isDesktopShell } from '@/lib/desktop';
import { Icon } from '@/components/icon/Icon';
import { SettingsSection } from '@/components/sections/shared/SettingsSection';
import { LinearProjectMapping } from './LinearProjectMapping';
import { LinearSessionComments } from './LinearSessionComments';
@@ -172,13 +171,6 @@ export const LinearSettings: React.FC = () => {
const expanded = isWaiting || open;
return (
<SettingsSection
title={t('settings.integrations.firstParty.title')}
info={t('settings.integrations.firstParty.info')}
divider={false}
settingsItem="integrations.first-party"
contentClassName="space-y-3"
>
<Collapsible
open={expanded}
onOpenChange={(nextOpen) => {
@@ -345,6 +337,5 @@ export const LinearSettings: React.FC = () => {
</CollapsibleContent>
</div>
</Collapsible>
</SettingsSection>
);
};
@@ -34,7 +34,12 @@ type DeviceFlowCompleteResponse =
| { connected: true; user: GitHubUser; scope?: string }
| { connected: false; status?: string; error?: string };
export const GitHubSettings: React.FC = () => {
type GitHubSettingsProps = {
/** Rendered inside the Integrations card: no section chrome of its own. */
embedded?: boolean;
};
export const GitHubSettings: React.FC<GitHubSettingsProps> = ({ embedded = false }) => {
const { t } = useI18n();
const { isMobile } = useDeviceInfo();
const runtimeGitHub = getRegisteredRuntimeAPIs()?.github;
@@ -269,14 +274,8 @@ export const GitHubSettings: React.FC = () => {
? t('settings.github.page.accountSource.cli')
: t('settings.github.page.accountSource.oauth');
return (
const accountSection = (
<>
<SettingsSection
title={t('settings.github.page.oauth.title')}
divider={false}
settingsItem="git.github-account"
info={t('settings.github.page.tooltip.connectAccount')}
>
<div className="rounded-lg bg-[var(--surface-elevated)]/70 overflow-hidden flex flex-col">
{connected ? (
<div className={cn("px-4 py-3", isMobile ? "flex flex-col gap-3" : "flex items-center justify-between gap-4")}>
@@ -445,10 +444,12 @@ export const GitHubSettings: React.FC = () => {
</div>
)}
</SettingsSection>
</>
);
{ghCli?.available && !ghCli?.active && (!ghCli.user || ghCli.disabled) && (
<SettingsSection title={t('settings.github.page.ghCli.title')}>
const ghCliSection = ghCli?.available && !ghCli?.active && (!ghCli.user || ghCli.disabled)
? (
<>
<div className="rounded-lg bg-[var(--surface-elevated)]/70 overflow-hidden">
<div className={cn("px-4 py-3", isMobile ? "flex flex-col gap-3" : "flex items-center justify-between gap-4")}>
<div className={cn("flex min-w-0 items-center gap-4", isMobile ? "w-full" : undefined)}>
@@ -499,8 +500,40 @@ export const GitHubSettings: React.FC = () => {
</Button>
</div>
</div>
</>
)
: null;
if (embedded) {
return (
<div className="space-y-4">
{accountSection}
{ghCliSection ? (
<div className="space-y-2">
<SettingsGroupTitle>{t('settings.github.page.ghCli.title')}</SettingsGroupTitle>
{ghCliSection}
</div>
) : null}
</div>
);
}
return (
<>
<SettingsSection
title={t('settings.github.page.oauth.title')}
divider={false}
settingsItem="git.github-account"
info={t('settings.github.page.tooltip.connectAccount')}
>
{accountSection}
</SettingsSection>
{ghCliSection ? (
<SettingsSection title={t('settings.github.page.ghCli.title')}>
{ghCliSection}
</SettingsSection>
)}
) : null}
</>
);
};
@@ -7,7 +7,6 @@ import { AppLinkSecuritySettings } from './AppLinkSecuritySettings';
import { DefaultsSettings } from './DefaultsSettings';
import { GitSettings } from './GitSettings';
import { NotificationSettings } from './NotificationSettings';
import { GitHubSettings } from './GitHubSettings';
import { VoiceSettings } from './VoiceSettings';
import { TunnelSettings } from './TunnelSettings';
import { OpenCodeCliSettings } from './OpenCodeCliSettings';
@@ -78,8 +77,6 @@ export const OpenChamberPage: React.FC<OpenChamberPageProps> = ({ section }) =>
return <ShortcutsSectionContent />;
case 'git':
return <GitSectionContent />;
case 'github':
return <GitHubSectionContent />;
case 'notifications':
return <NotificationSectionContent />;
case 'voice':
@@ -233,14 +230,6 @@ const GitSectionContent: React.FC = () => {
return <GitSettings />;
};
// GitHub section: Connect account for PR/issue workflows
const GitHubSectionContent: React.FC = () => {
if (isVSCodeRuntime()) {
return null;
}
return <GitHubSettings />;
};
// Notifications section: Native browser notifications
const NotificationSectionContent: React.FC = () => {
return <NotificationSettings />;