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:
@@ -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 />;
|
||||
|
||||
Reference in New Issue
Block a user