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
@@ -1,6 +1,9 @@
/** Linear first-party integration settings strings — merged into each locale's settings dictionary. */
/** Built-in integration (GitHub, Linear) settings strings — merged into each locale's settings dictionary. */
export const linearIntegrationI18n = {
en: {
'settings.integrations.github.title': 'GitHub',
'settings.integrations.github.description': 'Connect a GitHub account for pull requests and issues.',
'settings.integrations.github.status.notConnected': 'Not connected',
'settings.integrations.firstParty.title': 'Built-in integrations',
'settings.integrations.firstParty.info': 'Sign-ins for services that ship with OpenChamber. The login stays on this computer so web, desktop, and a paired phone share it.',
'settings.integrations.linear.title': 'Linear',
@@ -48,6 +51,9 @@ export const linearIntegrationI18n = {
'settings.magicPrompts.page.group.linearIssueReview.description': 'Prompts used when starting a session from a Linear issue: visible user message + hidden instructions.',
},
de: {
'settings.integrations.github.title': 'GitHub',
'settings.integrations.github.description': 'GitHub-Konto für Pull Requests und Issues verbinden.',
'settings.integrations.github.status.notConnected': 'Nicht verbunden',
'settings.integrations.firstParty.title': 'Eingebaute Integrationen',
'settings.integrations.firstParty.info': 'Anmeldungen für Dienste, die mit OpenChamber mitgeliefert werden. Die Anmeldung bleibt auf diesem Computer, damit Web, Desktop und ein gekoppeltes Telefon sie teilen.',
'settings.integrations.linear.title': 'Linear',
@@ -95,6 +101,9 @@ export const linearIntegrationI18n = {
'settings.magicPrompts.page.group.linearIssueReview.description': 'Eingabeaufforderungen beim Start einer Sitzung aus einem Linear-Issue: sichtbare Benutzernachricht + versteckte Anweisungen.',
},
fr: {
'settings.integrations.github.title': 'GitHub',
'settings.integrations.github.description': 'Connecter un compte GitHub pour les pull requests et les issues.',
'settings.integrations.github.status.notConnected': 'Non connecté',
'settings.integrations.firstParty.title': 'Intégrations natives',
'settings.integrations.firstParty.info': 'Connexions aux services fournis avec OpenChamber. La connexion reste sur cet ordinateur pour que le web, le bureau et un téléphone apparié la partagent.',
'settings.integrations.linear.title': 'Linear',
@@ -142,6 +151,9 @@ export const linearIntegrationI18n = {
'settings.magicPrompts.page.group.linearIssueReview.description': 'Prompts utilisés au démarrage dune session depuis un ticket Linear : message utilisateur visible + instructions masquées.',
},
es: {
'settings.integrations.github.title': 'GitHub',
'settings.integrations.github.description': 'Conecta una cuenta de GitHub para pull requests e issues.',
'settings.integrations.github.status.notConnected': 'No conectado',
'settings.integrations.firstParty.title': 'Integraciones nativas',
'settings.integrations.firstParty.info': 'Inicios de sesión de los servicios incluidos en OpenChamber. El inicio de sesión se guarda en este ordenador para que la web, el escritorio y un teléfono emparejado lo compartan.',
'settings.integrations.linear.title': 'Linear',
@@ -189,6 +201,9 @@ export const linearIntegrationI18n = {
'settings.magicPrompts.page.group.linearIssueReview.description': 'Prompts usados al iniciar una sesión desde un issue de Linear: mensaje visible del usuario e instrucciones ocultas.',
},
ja: {
'settings.integrations.github.title': 'GitHub',
'settings.integrations.github.description': 'プルリクエストと Issue のために GitHub アカウントを接続します。',
'settings.integrations.github.status.notConnected': '未接続',
'settings.integrations.firstParty.title': '標準連携',
'settings.integrations.firstParty.info': 'OpenChamber に同梱されているサービスのログインです。このコンピュータに保存され、Web、デスクトップ、ペアリングしたスマホで共有されます。',
'settings.integrations.linear.title': 'Linear',
@@ -236,6 +251,9 @@ export const linearIntegrationI18n = {
'settings.magicPrompts.page.group.linearIssueReview.description': 'Linear の Issue からセッションを開始するときに使うプロンプト: 表示ユーザーメッセージ + 非表示の指示。',
},
ko: {
'settings.integrations.github.title': 'GitHub',
'settings.integrations.github.description': '풀 리퀘스트와 이슈를 위해 GitHub 계정을 연결합니다.',
'settings.integrations.github.status.notConnected': '연결되지 않음',
'settings.integrations.firstParty.title': '기본 제공 통합',
'settings.integrations.firstParty.info': 'OpenChamber에 포함된 서비스 로그인입니다. 이 컴퓨터에 저장되며 웹, 데스크톱, 페어링된 휴대폰이 공유합니다.',
'settings.integrations.linear.title': 'Linear',
@@ -283,6 +301,9 @@ export const linearIntegrationI18n = {
'settings.magicPrompts.page.group.linearIssueReview.description': 'Linear 이슈로 세션을 시작할 때 쓰는 프롬프트입니다: 표시 사용자 메시지 + 숨겨진 지침.',
},
pl: {
'settings.integrations.github.title': 'GitHub',
'settings.integrations.github.description': 'Połącz konto GitHub dla pull requestów i issues.',
'settings.integrations.github.status.notConnected': 'Nie połączono',
'settings.integrations.firstParty.title': 'Wbudowane integracje',
'settings.integrations.firstParty.info': 'Logowania do usług dostarczanych z OpenChamber. Zapisujemy je na tym komputerze, żeby przeglądarka, aplikacja desktopowa i sparowany telefon z nich korzystały.',
'settings.integrations.linear.title': 'Linear',
@@ -330,6 +351,9 @@ export const linearIntegrationI18n = {
'settings.magicPrompts.page.group.linearIssueReview.description': 'Prompty używane przy starcie sesji ze zgłoszenia Linear: widoczna wiadomość użytkownika i ukryte instrukcje.',
},
'pt-BR': {
'settings.integrations.github.title': 'GitHub',
'settings.integrations.github.description': 'Conecte uma conta do GitHub para pull requests e issues.',
'settings.integrations.github.status.notConnected': 'Não conectado',
'settings.integrations.firstParty.title': 'Integrações nativas',
'settings.integrations.firstParty.info': 'Logins dos serviços inclusos no OpenChamber. O login fica neste computador para que a web, o app desktop e um celular emparelhado o compartilhem.',
'settings.integrations.linear.title': 'Linear',
@@ -377,6 +401,9 @@ export const linearIntegrationI18n = {
'settings.magicPrompts.page.group.linearIssueReview.description': 'Prompts usados ao iniciar uma sessão a partir de uma issue do Linear: mensagem visível do usuário e instruções ocultas.',
},
uk: {
'settings.integrations.github.title': 'GitHub',
'settings.integrations.github.description': 'Підключіть акаунт GitHub для pull request-ів та issues.',
'settings.integrations.github.status.notConnected': 'Не підключено',
'settings.integrations.firstParty.title': 'Вбудовані інтеграції',
'settings.integrations.firstParty.info': 'Входи до сервісів, що входять до OpenChamber. Логін лишається на цьому комп’ютері, тож веб, десктоп і спарений телефон користуються одним обліковим записом.',
'settings.integrations.linear.title': 'Linear',
@@ -424,6 +451,9 @@ export const linearIntegrationI18n = {
'settings.magicPrompts.page.group.linearIssueReview.description': 'Промпти для старту сесії з Linear issue: видиме повідомлення користувача та приховані інструкції.',
},
'zh-CN': {
'settings.integrations.github.title': 'GitHub',
'settings.integrations.github.description': '连接 GitHub 账号以处理拉取请求和议题。',
'settings.integrations.github.status.notConnected': '未连接',
'settings.integrations.firstParty.title': '内置集成',
'settings.integrations.firstParty.info': 'OpenChamber 自带服务的登录。登录保存在这台电脑上,网页、桌面应用和已配对的手机会共用它。',
'settings.integrations.linear.title': 'Linear',
@@ -471,6 +501,9 @@ export const linearIntegrationI18n = {
'settings.magicPrompts.page.group.linearIssueReview.description': '从 Linear Issue 开始会话时使用的提示词:可见用户消息 + 隐藏指令。',
},
'zh-TW': {
'settings.integrations.github.title': 'GitHub',
'settings.integrations.github.description': '連接 GitHub 帳號以處理拉取請求與議題。',
'settings.integrations.github.status.notConnected': '未連接',
'settings.integrations.firstParty.title': '內建整合',
'settings.integrations.firstParty.info': 'OpenChamber 內建服務的登入。登入保存在這台電腦上,網頁、桌面應用程式和已配對的手機會共用它。',
'settings.integrations.linear.title': 'Linear',
@@ -518,6 +551,9 @@ export const linearIntegrationI18n = {
'settings.magicPrompts.page.group.linearIssueReview.description': '從 Linear Issue 開始會話時使用的提示詞:可見使用者訊息 + 隱藏指令。',
},
tr: {
'settings.integrations.github.title': 'GitHub',
'settings.integrations.github.description': "Pull request ve issue'lar için bir GitHub hesabı bağlayın.",
'settings.integrations.github.status.notConnected': 'Bağlı değil',
'settings.integrations.firstParty.title': 'Yerleşik entegrasyonlar',
'settings.integrations.firstParty.info': 'OpenChamber ile gelen hizmetlerin oturumları. Giriş bu bilgisayarda kalır; web, masaüstü ve eşlenen telefon paylaşır.',
'settings.integrations.linear.title': 'Linear',
+2 -2
View File
@@ -150,7 +150,7 @@ export const SETTINGS_PAGE_METADATA: readonly SettingsPageMeta[] = [
title: 'Git',
group: 'projects',
kind: 'single',
keywords: ['git', 'github', 'identity', 'identities', 'ssh', 'profiles', 'credentials', 'keys', 'commit', 'gitmoji', 'oauth', 'prs', 'issues'],
keywords: ['git', 'identity', 'identities', 'ssh', 'profiles', 'credentials', 'keys', 'commit', 'gitmoji'],
isAvailable: (ctx) => !ctx.isVSCode,
},
{
@@ -202,7 +202,7 @@ export const SETTINGS_PAGE_METADATA: readonly SettingsPageMeta[] = [
{ slug: 'voice', title: 'Voice', group: 'general', kind: 'single', keywords: ['tts', 'speech', 'voice'], isAvailable: (ctx) => !ctx.isVSCode },
{ slug: 'tunnel', title: 'External Tunnel', group: 'projects', kind: 'single', keywords: ['tunnel', 'external', 'cloudflare', 'qr', 'remote', 'mobile', 'share'], isAvailable: (ctx) => !ctx.isVSCode },
{ slug: 'about', title: 'About', group: 'general', kind: 'single', keywords: ['about', 'version', 'updates', 'release', 'changelog'], isAvailable: (ctx) => ctx.isMobile && !ctx.isVSCode },
{ slug: 'integrations', title: 'Integrations', group: 'general', kind: 'single', keywords: ['integration', 'plugin', 'provider', 'oauth', 'claude', 'cursor', 'command code', 'connect', 'discord', 'telegram', 'messenger', 'linear'] },
{ slug: 'integrations', title: 'Integrations', group: 'general', kind: 'single', keywords: ['integration', 'plugin', 'provider', 'oauth', 'claude', 'cursor', 'command code', 'connect', 'discord', 'telegram', 'messenger', 'github', 'linear'] },
] as const;
const LEGACY_SIDEBAR_SECTION_TO_SETTINGS_SLUG: Record<SidebarSection, SettingsPageSlug> = {
+9 -7
View File
@@ -527,12 +527,6 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
// user to an empty spot on the page.
isAvailable: (ctx) => !ctx.isVSCode && useUIStore.getState().agentMemoryFeatureAvailable,
},
{
id: 'git.github-account',
page: 'git',
titleKey: 'settings.github.page.actions.connect',
keywords: ['github', 'account', 'oauth', 'prs', 'issues'],
},
{
id: 'git.identities',
page: 'git',
@@ -989,7 +983,15 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
page: 'integrations',
titleKey: 'settings.integrations.firstParty.title',
descriptionKey: 'settings.integrations.firstParty.info',
keywords: ['built-in', 'first-party', 'native', 'linear'],
keywords: ['built-in', 'first-party', 'native', 'github', 'linear'],
isAvailable: (ctx) => !ctx.isVSCode,
},
{
id: 'integrations.github',
page: 'integrations',
titleKey: 'settings.integrations.github.title',
descriptionKey: 'settings.integrations.github.description',
keywords: ['github', 'account', 'oauth', 'gh', 'cli', 'prs', 'pull request', 'issues', 'connect'],
isAvailable: (ctx) => !ctx.isVSCode,
},
{
@@ -27,7 +27,9 @@ edge (`components/layout/ContextPanelRail.tsx`) and rendered by
configure button — `ContextRailSurfacesDialog`), drops the plan surface
unless plan mode is enabled,
drops the walkthrough on VS Code and below `WALKTHROUGH_MIN_WIDTH`, hides
Linear unless a workspace is connected, and hides `has-content` surfaces
Linear unless a workspace is connected, hides the pull-request surface
unless GitHub is connected (OAuth or `gh` CLI — signed in from Settings →
Integrations), and hides `has-content` surfaces
until a tab of their mode exists. Both consumers use it so the digit shown
on a rail badge always maps to the same surface the shortcut opens.
+10 -5
View File
@@ -13,6 +13,7 @@ const baseOptions = {
screenWidth: 1200,
tabs: [],
linearConnected: true,
githubConnected: true,
} as const;
describe('getVisibleContextRailSurfaces', () => {
@@ -65,12 +66,16 @@ describe('getVisibleContextRailSurfaces', () => {
expect(surfaces.slice(0, 2).map((surface) => surface.id)).toEqual(['git', 'context']);
});
test('places Linear after Pull Request in the default order', () => {
test('places Linear right after the walkthrough in the default order', () => {
const ids = getVisibleContextRailSurfaces(baseOptions).map((surface) => surface.id);
const pr = ids.indexOf('pr');
const linear = ids.indexOf('linear');
expect(pr).toBeGreaterThanOrEqual(0);
expect(linear).toBe(pr + 1);
const walkthrough = ids.indexOf('walkthrough');
expect(walkthrough).toBeGreaterThanOrEqual(0);
expect(ids.indexOf('linear')).toBe(walkthrough + 1);
});
test('hides the pull request surface until GitHub is connected', () => {
expect(getVisibleContextRailSurfaces({ ...baseOptions, githubConnected: false }).some((s) => s.id === 'pr')).toBe(false);
expect(getVisibleContextRailSurfaces({ ...baseOptions, githubConnected: true }).some((s) => s.id === 'pr')).toBe(true);
});
test('hides Linear until a workspace is connected', () => {
+16 -9
View File
@@ -66,15 +66,6 @@ export const CONTEXT_SURFACES: readonly ContextSurfaceDescriptor[] = [
labelKey: 'contextPanel.mode.pr',
availability: 'always',
},
{
id: 'linear',
descriptionKey: 'contextRail.surface.linear.description',
defaultWidthFraction: 0.45,
mode: 'linear',
icon: 'linear',
labelKey: 'contextPanel.mode.linear',
availability: 'always',
},
{
id: 'diff',
descriptionKey: 'contextRail.surface.diff.description',
@@ -93,6 +84,15 @@ export const CONTEXT_SURFACES: readonly ContextSurfaceDescriptor[] = [
labelKey: 'contextPanel.mode.walkthrough',
availability: 'always',
},
{
id: 'linear',
descriptionKey: 'contextRail.surface.linear.description',
defaultWidthFraction: 0.45,
mode: 'linear',
icon: 'linear',
labelKey: 'contextPanel.mode.linear',
availability: 'always',
},
{
id: 'editor',
descriptionKey: 'contextRail.surface.editor.description',
@@ -206,6 +206,10 @@ type VisibleRailSurfacesOptions = {
tabs: readonly { mode: ContextPanelMode }[];
/** Linear's rail icon stays off until a workspace is connected. */
linearConnected: boolean;
/** The pull-request rail icon stays off until GitHub is connected (OAuth
or a detected `gh` CLI login). GitHub is connected from Settings, so
hiding the surface removes no entry point. */
githubConnected: boolean;
};
/**
@@ -240,6 +244,9 @@ export const getVisibleContextRailSurfaces = (options: VisibleRailSurfacesOption
if (surface.id === 'linear' && !options.linearConnected) {
return false;
}
if (surface.id === 'pr' && !options.githubConnected) {
return false;
}
if (surface.availability === 'has-content') {
return options.tabs.some((tab) => tab.mode === surface.mode);
}