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:
@@ -0,0 +1,161 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Icon } from '@/components/icon/Icon';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||||
|
import type { GitHubAuthStatus } from '@/lib/api/types';
|
||||||
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
||||||
|
|
||||||
|
type GitHubAccount = NonNullable<GitHubAuthStatus['accounts']>[number];
|
||||||
|
|
||||||
|
const AVATAR_CLASS = 'flex h-6 w-6 items-center justify-center overflow-hidden rounded-full border border-border/60 bg-muted/80';
|
||||||
|
|
||||||
|
const activateAccount = async (
|
||||||
|
github: ReturnType<typeof useRuntimeAPIs>['github'],
|
||||||
|
accountId: string,
|
||||||
|
): Promise<GitHubAuthStatus> => {
|
||||||
|
if (github) {
|
||||||
|
return github.authActivate(accountId);
|
||||||
|
}
|
||||||
|
const response = await runtimeFetch('/api/github/auth/activate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||||
|
body: JSON.stringify({ accountId }),
|
||||||
|
});
|
||||||
|
// SAFETY: the route is ours and answers the auth status shape (plus an
|
||||||
|
// `error` string on failure) on every response; a non-ok status throws below.
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The connected GitHub account: an avatar, and a switcher when more than one
|
||||||
|
* account is signed in (OAuth and `gh` CLI logins). Renders nothing while
|
||||||
|
* GitHub is disconnected — connecting happens in Settings → Integrations.
|
||||||
|
*/
|
||||||
|
export const GitHubAccountControl: React.FC<{ className?: string }> = ({ className }) => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { github } = useRuntimeAPIs();
|
||||||
|
const status = useGitHubAuthStore((state) => state.status);
|
||||||
|
const setStatus = useGitHubAuthStore((state) => state.setStatus);
|
||||||
|
const [isSwitching, setIsSwitching] = React.useState(false);
|
||||||
|
|
||||||
|
const switchAccount = React.useCallback(async (accountId: string) => {
|
||||||
|
if (!accountId || isSwitching) return;
|
||||||
|
setIsSwitching(true);
|
||||||
|
try {
|
||||||
|
setStatus(await activateAccount(github, accountId));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to switch GitHub account:', error);
|
||||||
|
} finally {
|
||||||
|
setIsSwitching(false);
|
||||||
|
}
|
||||||
|
}, [github, isSwitching, setStatus]);
|
||||||
|
|
||||||
|
if (!status?.connected) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = status.user?.login ?? null;
|
||||||
|
const avatarUrl = status.user?.avatarUrl ?? null;
|
||||||
|
const accounts: GitHubAccount[] = status.accounts ?? [];
|
||||||
|
const title = login ? t('header.github.connectedWithLogin', { login }) : t('header.github.connected');
|
||||||
|
const avatar = avatarUrl ? (
|
||||||
|
<img
|
||||||
|
src={avatarUrl}
|
||||||
|
alt={login ? t('header.github.avatarWithLogin', { login }) : 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" />
|
||||||
|
);
|
||||||
|
|
||||||
|
if (accounts.length <= 1) {
|
||||||
|
return (
|
||||||
|
<div className={cn(AVATAR_CLASS, className)} title={title}>
|
||||||
|
{avatar}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(AVATAR_CLASS, 'p-0 hover:ring-2 hover:ring-primary/40 disabled:opacity-50', className)}
|
||||||
|
title={title}
|
||||||
|
disabled={isSwitching}
|
||||||
|
>
|
||||||
|
{avatar}
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-64">
|
||||||
|
<DropdownMenuLabel className="typography-ui-header font-semibold text-foreground">
|
||||||
|
{t('header.github.accountsTitle')}
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{accounts.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={isSwitching}
|
||||||
|
onSelect={() => {
|
||||||
|
if (!isCurrent) {
|
||||||
|
void switchAccount(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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
|
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
|
||||||
import { useGitStatus } from '@/stores/useGitStore';
|
import { useGitStatus } from '@/stores/useGitStore';
|
||||||
|
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
||||||
import { useLinearAuthStore } from '@/stores/useLinearAuthStore';
|
import { useLinearAuthStore } from '@/stores/useLinearAuthStore';
|
||||||
import { normalizeContextPanelDirectoryKey, useUIStore } from '@/stores/useUIStore';
|
import { normalizeContextPanelDirectoryKey, useUIStore } from '@/stores/useUIStore';
|
||||||
import { ContextRailSurfacesDialog } from './ContextRailSurfacesDialog';
|
import { ContextRailSurfacesDialog } from './ContextRailSurfacesDialog';
|
||||||
@@ -171,6 +172,8 @@ export const ContextPanelRail: React.FC = () => {
|
|||||||
const planModeEnabled = useFeatureFlagsStore((state) => state.planModeEnabled);
|
const planModeEnabled = useFeatureFlagsStore((state) => state.planModeEnabled);
|
||||||
const linearAuthChecked = useLinearAuthStore((state) => state.hasChecked);
|
const linearAuthChecked = useLinearAuthStore((state) => state.hasChecked);
|
||||||
const linearConnected = useLinearAuthStore((state) => state.status?.connected === true);
|
const linearConnected = useLinearAuthStore((state) => state.status?.connected === true);
|
||||||
|
const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked);
|
||||||
|
const githubConnected = useGitHubAuthStore((state) => state.status?.connected === true);
|
||||||
const { screenWidth } = useDeviceInfo();
|
const { screenWidth } = useDeviceInfo();
|
||||||
const gitStatus = useGitStatus(directoryKey || null);
|
const gitStatus = useGitStatus(directoryKey || null);
|
||||||
|
|
||||||
@@ -268,9 +271,12 @@ export const ContextPanelRail: React.FC = () => {
|
|||||||
screenWidth,
|
screenWidth,
|
||||||
tabs,
|
tabs,
|
||||||
linearConnected,
|
linearConnected,
|
||||||
|
githubConnected,
|
||||||
});
|
});
|
||||||
}, [contextRailHiddenSurfaces, contextRailOrder, linearConnected, planModeEnabled, screenWidth, tabs]);
|
}, [contextRailHiddenSurfaces, contextRailOrder, githubConnected, linearConnected, planModeEnabled, screenWidth, tabs]);
|
||||||
|
|
||||||
|
// A surface whose integration disconnected closes rather than lingering as
|
||||||
|
// an active panel with no rail icon.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!directoryKey || !linearAuthChecked || linearConnected || activeMode !== 'linear') {
|
if (!directoryKey || !linearAuthChecked || linearConnected || activeMode !== 'linear') {
|
||||||
return;
|
return;
|
||||||
@@ -278,6 +284,13 @@ export const ContextPanelRail: React.FC = () => {
|
|||||||
closeContextPanel(directoryKey);
|
closeContextPanel(directoryKey);
|
||||||
}, [activeMode, closeContextPanel, directoryKey, linearAuthChecked, linearConnected]);
|
}, [activeMode, closeContextPanel, directoryKey, linearAuthChecked, linearConnected]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!directoryKey || !githubAuthChecked || githubConnected || activeMode !== 'pr') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closeContextPanel(directoryKey);
|
||||||
|
}, [activeMode, closeContextPanel, directoryKey, githubAuthChecked, githubConnected]);
|
||||||
|
|
||||||
const [isSurfacesDialogOpen, setIsSurfacesDialogOpen] = React.useState(false);
|
const [isSurfacesDialogOpen, setIsSurfacesDialogOpen] = React.useState(false);
|
||||||
|
|
||||||
const handleDragEnd = React.useCallback((event: DragEndEvent) => {
|
const handleDragEnd = React.useCallback((event: DragEndEvent) => {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
@@ -30,7 +29,6 @@ import { useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore';
|
|||||||
import { streamPerfCount } from '@/stores/utils/streamDebug';
|
import { streamPerfCount } from '@/stores/utils/streamDebug';
|
||||||
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
|
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
|
||||||
|
|
||||||
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
|
||||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||||
import { useDesktopWindowControlsLayout } from '@/hooks/useDesktopWindowControlsLayout';
|
import { useDesktopWindowControlsLayout } from '@/hooks/useDesktopWindowControlsLayout';
|
||||||
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
|
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
|
||||||
@@ -45,7 +43,6 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
} from '@/components/ui/collapsible';
|
} from '@/components/ui/collapsible';
|
||||||
import type { GitHubAuthStatus } from '@/lib/api/types';
|
|
||||||
import type { SessionContextUsage } from '@/stores/types/sessionTypes';
|
import type { SessionContextUsage } from '@/stores/types/sessionTypes';
|
||||||
import { DesktopHostSwitcherDialog } from '@/components/desktop/DesktopHostSwitcher';
|
import { DesktopHostSwitcherDialog } from '@/components/desktop/DesktopHostSwitcher';
|
||||||
import { OpenInAppButton } from '@/components/desktop/OpenInAppButton';
|
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 = {
|
type DesktopServicesMenuProps = {
|
||||||
isDesktopApp: boolean;
|
isDesktopApp: boolean;
|
||||||
currentInstanceLabel: string;
|
currentInstanceLabel: string;
|
||||||
@@ -439,7 +310,6 @@ export const Header: React.FC = () => {
|
|||||||
const sessionTabsEnabled = useUIStore((state) => state.sessionTabsEnabled);
|
const sessionTabsEnabled = useUIStore((state) => state.sessionTabsEnabled);
|
||||||
|
|
||||||
const getCurrentModel = useConfigStore((state) => state.getCurrentModel);
|
const getCurrentModel = useConfigStore((state) => state.getCurrentModel);
|
||||||
const runtimeApis = useRuntimeAPIs();
|
|
||||||
|
|
||||||
const getContextUsage = useSessionUIStore((state) => state.getContextUsage);
|
const getContextUsage = useSessionUIStore((state) => state.getContextUsage);
|
||||||
const isNewSessionDraftOpen = useSessionUIStore((state) => Boolean(state.newSessionDraft?.open));
|
const isNewSessionDraftOpen = useSessionUIStore((state) => Boolean(state.newSessionDraft?.open));
|
||||||
@@ -488,8 +358,6 @@ export const Header: React.FC = () => {
|
|||||||
const loadQuotaSettings = useQuotaStore((state) => state.loadSettings);
|
const loadQuotaSettings = useQuotaStore((state) => state.loadSettings);
|
||||||
|
|
||||||
const { isMobile } = useDeviceInfo();
|
const { isMobile } = useDeviceInfo();
|
||||||
const githubAuthStatus = useGitHubAuthStore((state) => state.status);
|
|
||||||
const setGitHubAuthStatus = useGitHubAuthStore((state) => state.setStatus);
|
|
||||||
|
|
||||||
const headerRef = React.useRef<HTMLElement | null>(null);
|
const headerRef = React.useRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
@@ -571,10 +439,6 @@ export const Header: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [contextUsage, currentSessionId, isContextUsageResolvedForSession]);
|
}, [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 [isDesktopServicesOpen, setIsDesktopServicesOpen] = React.useState(false);
|
||||||
const [currentInstanceLabel, setCurrentInstanceLabel] = React.useState('Local');
|
const [currentInstanceLabel, setCurrentInstanceLabel] = React.useState('Local');
|
||||||
const [currentInstanceIsLocal, setCurrentInstanceIsLocal] = React.useState(true);
|
const [currentInstanceIsLocal, setCurrentInstanceIsLocal] = React.useState(true);
|
||||||
@@ -1183,37 +1047,6 @@ export const Header: React.FC = () => {
|
|||||||
sessionDirectory,
|
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}
|
onOpenRemoteUpdate={openRemoteInstanceUpdate}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<DesktopGitHubControl
|
|
||||||
isMobile={isMobile}
|
|
||||||
githubAuthStatus={githubAuthStatus}
|
|
||||||
githubAccounts={githubAccounts}
|
|
||||||
githubAvatarUrl={githubAvatarUrl}
|
|
||||||
githubLogin={githubLogin}
|
|
||||||
isSwitchingGitHubAccount={isSwitchingGitHubAccount}
|
|
||||||
handleGitHubAccountSwitch={handleGitHubAccountSwitch}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger }
|
|||||||
import { useGitIdentitiesStore, type GitIdentityProfile, type DiscoveredGitCredential } from '@/stores/useGitIdentitiesStore';
|
import { useGitIdentitiesStore, type GitIdentityProfile, type DiscoveredGitCredential } from '@/stores/useGitIdentitiesStore';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { GitSettings } from '@/components/sections/openchamber/GitSettings';
|
import { GitSettings } from '@/components/sections/openchamber/GitSettings';
|
||||||
import { GitHubSettings } from '@/components/sections/openchamber/GitHubSettings';
|
|
||||||
import { GitIdentityEditorDialog } from './GitIdentityEditorDialog';
|
import { GitIdentityEditorDialog } from './GitIdentityEditorDialog';
|
||||||
import { Icon } from "@/components/icon/Icon";
|
import { Icon } from "@/components/icon/Icon";
|
||||||
import type { IconName } from "@/components/icon/icons";
|
import type { IconName } from "@/components/icon/icons";
|
||||||
@@ -121,10 +120,9 @@ export const GitPage: React.FC = () => {
|
|||||||
title={t('settings.page.git.title')}
|
title={t('settings.page.git.title')}
|
||||||
showSaveStatus
|
showSaveStatus
|
||||||
>
|
>
|
||||||
<GitHubSettings />
|
|
||||||
|
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
title={t('settings.gitIdentities.page.section.title')}
|
title={t('settings.gitIdentities.page.section.title')}
|
||||||
|
divider={false}
|
||||||
headerAction={(
|
headerAction={(
|
||||||
<Button size="sm" variant="outline" onClick={() => openEditor('new')}>
|
<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')}
|
<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 React from 'react';
|
||||||
import { SettingsPageLayout } from '@/components/sections/shared/SettingsPageLayout';
|
import { SettingsPageLayout } from '@/components/sections/shared/SettingsPageLayout';
|
||||||
|
import { SettingsSection } from '@/components/sections/shared/SettingsSection';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||||
|
import { GitHubIntegration } from './GitHubIntegration';
|
||||||
import { LinearSettings } from './LinearSettings';
|
import { LinearSettings } from './LinearSettings';
|
||||||
import { ThirdPartyIntegrationsSection } from './ThirdPartyIntegrationsSection';
|
import { ThirdPartyIntegrationsSection } from './ThirdPartyIntegrationsSection';
|
||||||
|
|
||||||
@@ -15,7 +18,11 @@ export const IntegrationsPage: React.FC<IntegrationsPageProps> = ({
|
|||||||
onOpenPluginManager,
|
onOpenPluginManager,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
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 hasLinear = Boolean(getRegisteredRuntimeAPIs()?.linear);
|
||||||
|
const hasBuiltIn = hasGitHub || hasLinear;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsPageLayout
|
<SettingsPageLayout
|
||||||
@@ -23,9 +30,20 @@ export const IntegrationsPage: React.FC<IntegrationsPageProps> = ({
|
|||||||
description={t('settings.page.integrations.description')}
|
description={t('settings.page.integrations.description')}
|
||||||
showSaveStatus
|
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
|
<ThirdPartyIntegrationsSection
|
||||||
divider={hasLinear}
|
divider={hasBuiltIn}
|
||||||
onOpenProviderSetup={onOpenProviderSetup}
|
onOpenProviderSetup={onOpenProviderSetup}
|
||||||
onOpenPluginManager={onOpenPluginManager}
|
onOpenPluginManager={onOpenPluginManager}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { openExternalUrl } from '@/lib/url';
|
|||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
import { focusDesktopWindow, isDesktopShell } from '@/lib/desktop';
|
import { focusDesktopWindow, isDesktopShell } from '@/lib/desktop';
|
||||||
import { Icon } from '@/components/icon/Icon';
|
import { Icon } from '@/components/icon/Icon';
|
||||||
import { SettingsSection } from '@/components/sections/shared/SettingsSection';
|
|
||||||
import { LinearProjectMapping } from './LinearProjectMapping';
|
import { LinearProjectMapping } from './LinearProjectMapping';
|
||||||
import { LinearSessionComments } from './LinearSessionComments';
|
import { LinearSessionComments } from './LinearSessionComments';
|
||||||
|
|
||||||
@@ -172,13 +171,6 @@ export const LinearSettings: React.FC = () => {
|
|||||||
const expanded = isWaiting || open;
|
const expanded = isWaiting || open;
|
||||||
|
|
||||||
return (
|
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
|
<Collapsible
|
||||||
open={expanded}
|
open={expanded}
|
||||||
onOpenChange={(nextOpen) => {
|
onOpenChange={(nextOpen) => {
|
||||||
@@ -345,6 +337,5 @@ export const LinearSettings: React.FC = () => {
|
|||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</div>
|
</div>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</SettingsSection>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,7 +34,12 @@ type DeviceFlowCompleteResponse =
|
|||||||
| { connected: true; user: GitHubUser; scope?: string }
|
| { connected: true; user: GitHubUser; scope?: string }
|
||||||
| { connected: false; status?: string; error?: 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 { t } = useI18n();
|
||||||
const { isMobile } = useDeviceInfo();
|
const { isMobile } = useDeviceInfo();
|
||||||
const runtimeGitHub = getRegisteredRuntimeAPIs()?.github;
|
const runtimeGitHub = getRegisteredRuntimeAPIs()?.github;
|
||||||
@@ -269,14 +274,8 @@ export const GitHubSettings: React.FC = () => {
|
|||||||
? t('settings.github.page.accountSource.cli')
|
? t('settings.github.page.accountSource.cli')
|
||||||
: t('settings.github.page.accountSource.oauth');
|
: 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">
|
<div className="rounded-lg bg-[var(--surface-elevated)]/70 overflow-hidden flex flex-col">
|
||||||
{connected ? (
|
{connected ? (
|
||||||
<div className={cn("px-4 py-3", isMobile ? "flex flex-col gap-3" : "flex items-center justify-between gap-4")}>
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</SettingsSection>
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
{ghCli?.available && !ghCli?.active && (!ghCli.user || ghCli.disabled) && (
|
const ghCliSection = ghCli?.available && !ghCli?.active && (!ghCli.user || ghCli.disabled)
|
||||||
<SettingsSection title={t('settings.github.page.ghCli.title')}>
|
? (
|
||||||
|
<>
|
||||||
<div className="rounded-lg bg-[var(--surface-elevated)]/70 overflow-hidden">
|
<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("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)}>
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</SettingsSection>
|
||||||
)}
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { AppLinkSecuritySettings } from './AppLinkSecuritySettings';
|
|||||||
import { DefaultsSettings } from './DefaultsSettings';
|
import { DefaultsSettings } from './DefaultsSettings';
|
||||||
import { GitSettings } from './GitSettings';
|
import { GitSettings } from './GitSettings';
|
||||||
import { NotificationSettings } from './NotificationSettings';
|
import { NotificationSettings } from './NotificationSettings';
|
||||||
import { GitHubSettings } from './GitHubSettings';
|
|
||||||
import { VoiceSettings } from './VoiceSettings';
|
import { VoiceSettings } from './VoiceSettings';
|
||||||
import { TunnelSettings } from './TunnelSettings';
|
import { TunnelSettings } from './TunnelSettings';
|
||||||
import { OpenCodeCliSettings } from './OpenCodeCliSettings';
|
import { OpenCodeCliSettings } from './OpenCodeCliSettings';
|
||||||
@@ -78,8 +77,6 @@ export const OpenChamberPage: React.FC<OpenChamberPageProps> = ({ section }) =>
|
|||||||
return <ShortcutsSectionContent />;
|
return <ShortcutsSectionContent />;
|
||||||
case 'git':
|
case 'git':
|
||||||
return <GitSectionContent />;
|
return <GitSectionContent />;
|
||||||
case 'github':
|
|
||||||
return <GitHubSectionContent />;
|
|
||||||
case 'notifications':
|
case 'notifications':
|
||||||
return <NotificationSectionContent />;
|
return <NotificationSectionContent />;
|
||||||
case 'voice':
|
case 'voice':
|
||||||
@@ -233,14 +230,6 @@ const GitSectionContent: React.FC = () => {
|
|||||||
return <GitSettings />;
|
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
|
// Notifications section: Native browser notifications
|
||||||
const NotificationSectionContent: React.FC = () => {
|
const NotificationSectionContent: React.FC = () => {
|
||||||
return <NotificationSettings />;
|
return <NotificationSettings />;
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ export function GitHubIntegrationDialog({
|
|||||||
const isGitHubConnected = githubAuthChecked && githubAuthStatus?.connected === true;
|
const isGitHubConnected = githubAuthChecked && githubAuthStatus?.connected === true;
|
||||||
|
|
||||||
const openGitHubSettings = () => {
|
const openGitHubSettings = () => {
|
||||||
setSettingsPage('github');
|
setSettingsPage('integrations');
|
||||||
setSettingsDialogOpen(true);
|
setSettingsDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ export function GitHubIssuePickerDialog({
|
|||||||
const repoUrl = result?.repo?.url ?? null;
|
const repoUrl = result?.repo?.url ?? null;
|
||||||
|
|
||||||
const openGitHubSettings = React.useCallback(() => {
|
const openGitHubSettings = React.useCallback(() => {
|
||||||
setSettingsPage('github');
|
setSettingsPage('integrations');
|
||||||
setSettingsDialogOpen(true);
|
setSettingsDialogOpen(true);
|
||||||
}, [setSettingsDialogOpen, setSettingsPage]);
|
}, [setSettingsDialogOpen, setSettingsPage]);
|
||||||
|
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ export function GitHubPrPickerDialog({
|
|||||||
const connected = githubAuthChecked ? result?.connected !== false : true;
|
const connected = githubAuthChecked ? result?.connected !== false : true;
|
||||||
|
|
||||||
const openGitHubSettings = React.useCallback(() => {
|
const openGitHubSettings = React.useCallback(() => {
|
||||||
setSettingsPage('github');
|
setSettingsPage('integrations');
|
||||||
setSettingsDialogOpen(true);
|
setSettingsDialogOpen(true);
|
||||||
}, [setSettingsDialogOpen, setSettingsPage]);
|
}, [setSettingsDialogOpen, setSettingsPage]);
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { useDeviceInfo } from '@/lib/device';
|
|||||||
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
|
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
|
||||||
import { SimpleMarkdownRenderer } from '@/components/chat/MarkdownRenderer';
|
import { SimpleMarkdownRenderer } from '@/components/chat/MarkdownRenderer';
|
||||||
import { Icon } from "@/components/icon/Icon";
|
import { Icon } from "@/components/icon/Icon";
|
||||||
|
import { GitHubAccountControl } from '@/components/github/GitHubAccountControl';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useWalkthroughStore } from '@/stores/useWalkthroughStore';
|
import { useWalkthroughStore } from '@/stores/useWalkthroughStore';
|
||||||
import { WALKTHROUGH_ACTION_CLASS } from '@/components/views/walkthrough/walkthroughAction';
|
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;
|
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 => {
|
const branchToTitle = (branch: string): string => {
|
||||||
return branch
|
return branch
|
||||||
@@ -337,7 +342,7 @@ export const PullRequestSection: React.FC<{
|
|||||||
const showWalkthroughAction = !isMobile && screenWidth >= 768 && !isVSCodeRuntime();
|
const showWalkthroughAction = !isMobile && screenWidth >= 768 && !isVSCodeRuntime();
|
||||||
|
|
||||||
const openGitHubSettings = React.useCallback(() => {
|
const openGitHubSettings = React.useCallback(() => {
|
||||||
setSettingsPage('github');
|
setSettingsPage('integrations');
|
||||||
setSettingsDialogOpen(true);
|
setSettingsDialogOpen(true);
|
||||||
}, [setSettingsDialogOpen, setSettingsPage]);
|
}, [setSettingsDialogOpen, setSettingsPage]);
|
||||||
|
|
||||||
@@ -1040,6 +1045,31 @@ export const PullRequestSection: React.FC<{
|
|||||||
await refreshPrStatus(prStatusKey, options);
|
await refreshPrStatus(prStatusKey, options);
|
||||||
}, [prStatusKey, refreshPrStatus]);
|
}, [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(() => {
|
const scheduleActionRefresh = React.useCallback(() => {
|
||||||
pendingActionRefreshTimersRef.current.forEach((timerId) => {
|
pendingActionRefreshTimersRef.current.forEach((timerId) => {
|
||||||
window.clearTimeout(timerId);
|
window.clearTimeout(timerId);
|
||||||
@@ -1406,7 +1436,10 @@ export const PullRequestSection: React.FC<{
|
|||||||
return (
|
return (
|
||||||
<section className="border-0 bg-transparent rounded-none">
|
<section className="border-0 bg-transparent rounded-none">
|
||||||
<div className="space-y-1 pt-3">
|
<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">
|
<div className="typography-micro text-muted-foreground">
|
||||||
{t('gitView.pullRequest.availableOnFeatureBranches')}
|
{t('gitView.pullRequest.availableOnFeatureBranches')}
|
||||||
</div>
|
</div>
|
||||||
@@ -1450,7 +1483,7 @@ export const PullRequestSection: React.FC<{
|
|||||||
return (
|
return (
|
||||||
<section className={containerClassName}>
|
<section className={containerClassName}>
|
||||||
<div className={headerClassName}>
|
<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">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
{pr ? (
|
{pr ? (
|
||||||
<Button
|
<Button
|
||||||
@@ -1472,27 +1505,47 @@ export const PullRequestSection: React.FC<{
|
|||||||
<span className="typography-meta text-muted-foreground truncate">#{pr.number}</span>
|
<span className="typography-meta text-muted-foreground truncate">#{pr.number}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 items-center gap-1">
|
<div className="flex shrink-0 items-center gap-1.5">
|
||||||
{isLoading ? <Icon name="loader-4" className="size-4 animate-spin text-muted-foreground" /> : null}
|
{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>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
className="inline-flex size-5 items-center justify-center rounded hover:bg-interactive-hover/60 disabled:opacity-40"
|
size="sm"
|
||||||
disabled={isLoading}
|
className="h-7 w-7 px-0"
|
||||||
onClick={() => void refresh({ force: true })}
|
disabled={isLoading || isManualRefreshing}
|
||||||
|
onClick={() => void refreshManually()}
|
||||||
aria-label={t('gitView.pr.actions.refreshAria')}
|
aria-label={t('gitView.pr.actions.refreshAria')}
|
||||||
>
|
>
|
||||||
<Icon name="refresh" className="size-3.5 text-muted-foreground" />
|
{isLoading || isManualRefreshing
|
||||||
</button>
|
? <Icon name="loader-4" className="size-4 animate-spin text-muted-foreground" />
|
||||||
|
: <Icon name="refresh" className="size-4 text-muted-foreground" />}
|
||||||
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent><p>{t('gitView.pr.actions.refresh')}</p></TooltipContent>
|
<TooltipContent><p>{t('gitView.pr.actions.refresh')}</p></TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<GitHubAccountControl className="h-7 w-7" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{pr ? (
|
{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">
|
<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>
|
<span style={{ color: prColorVar }}>{prStatusText}</span>
|
||||||
{checks ? (
|
{checks ? (
|
||||||
@@ -1508,23 +1561,6 @@ export const PullRequestSection: React.FC<{
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 items-center gap-1.5">
|
<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' ? (
|
{canMerge && pr.draft && pr.state === 'open' ? (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { readEmbeddedThemeSearchParams } from '@/contexts/theme-embedded-bootstr
|
|||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
|
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
|
||||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||||
|
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
||||||
import { useLinearAuthStore } from '@/stores/useLinearAuthStore';
|
import { useLinearAuthStore } from '@/stores/useLinearAuthStore';
|
||||||
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||||
import { getCycledPrimaryAgentName } from '@/components/chat/mobileControlsUtils';
|
import { getCycledPrimaryAgentName } from '@/components/chat/mobileControlsUtils';
|
||||||
@@ -507,6 +508,7 @@ export const useKeyboardShortcuts = () => {
|
|||||||
screenWidth: window.innerWidth,
|
screenWidth: window.innerWidth,
|
||||||
tabs: panel?.tabs ?? [],
|
tabs: panel?.tabs ?? [],
|
||||||
linearConnected: useLinearAuthStore.getState().status?.connected === true,
|
linearConnected: useLinearAuthStore.getState().status?.connected === true,
|
||||||
|
githubConnected: useGitHubAuthStore.getState().status?.connected === true,
|
||||||
});
|
});
|
||||||
const target = visibleSurfaces[switchSurfaceDigit - 1];
|
const target = visibleSurfaces[switchSurfaceDigit - 1];
|
||||||
if (target) {
|
if (target) {
|
||||||
|
|||||||
@@ -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 = {
|
export const linearIntegrationI18n = {
|
||||||
en: {
|
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.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.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',
|
'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.',
|
'settings.magicPrompts.page.group.linearIssueReview.description': 'Prompts used when starting a session from a Linear issue: visible user message + hidden instructions.',
|
||||||
},
|
},
|
||||||
de: {
|
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.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.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',
|
'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.',
|
'settings.magicPrompts.page.group.linearIssueReview.description': 'Eingabeaufforderungen beim Start einer Sitzung aus einem Linear-Issue: sichtbare Benutzernachricht + versteckte Anweisungen.',
|
||||||
},
|
},
|
||||||
fr: {
|
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.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.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',
|
'settings.integrations.linear.title': 'Linear',
|
||||||
@@ -142,6 +151,9 @@ export const linearIntegrationI18n = {
|
|||||||
'settings.magicPrompts.page.group.linearIssueReview.description': 'Prompts utilisés au démarrage d’une session depuis un ticket Linear : message utilisateur visible + instructions masquées.',
|
'settings.magicPrompts.page.group.linearIssueReview.description': 'Prompts utilisés au démarrage d’une session depuis un ticket Linear : message utilisateur visible + instructions masquées.',
|
||||||
},
|
},
|
||||||
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.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.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',
|
'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.',
|
'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: {
|
ja: {
|
||||||
|
'settings.integrations.github.title': 'GitHub',
|
||||||
|
'settings.integrations.github.description': 'プルリクエストと Issue のために GitHub アカウントを接続します。',
|
||||||
|
'settings.integrations.github.status.notConnected': '未接続',
|
||||||
'settings.integrations.firstParty.title': '標準連携',
|
'settings.integrations.firstParty.title': '標準連携',
|
||||||
'settings.integrations.firstParty.info': 'OpenChamber に同梱されているサービスのログインです。このコンピュータに保存され、Web、デスクトップ、ペアリングしたスマホで共有されます。',
|
'settings.integrations.firstParty.info': 'OpenChamber に同梱されているサービスのログインです。このコンピュータに保存され、Web、デスクトップ、ペアリングしたスマホで共有されます。',
|
||||||
'settings.integrations.linear.title': 'Linear',
|
'settings.integrations.linear.title': 'Linear',
|
||||||
@@ -236,6 +251,9 @@ export const linearIntegrationI18n = {
|
|||||||
'settings.magicPrompts.page.group.linearIssueReview.description': 'Linear の Issue からセッションを開始するときに使うプロンプト: 表示ユーザーメッセージ + 非表示の指示。',
|
'settings.magicPrompts.page.group.linearIssueReview.description': 'Linear の Issue からセッションを開始するときに使うプロンプト: 表示ユーザーメッセージ + 非表示の指示。',
|
||||||
},
|
},
|
||||||
ko: {
|
ko: {
|
||||||
|
'settings.integrations.github.title': 'GitHub',
|
||||||
|
'settings.integrations.github.description': '풀 리퀘스트와 이슈를 위해 GitHub 계정을 연결합니다.',
|
||||||
|
'settings.integrations.github.status.notConnected': '연결되지 않음',
|
||||||
'settings.integrations.firstParty.title': '기본 제공 통합',
|
'settings.integrations.firstParty.title': '기본 제공 통합',
|
||||||
'settings.integrations.firstParty.info': 'OpenChamber에 포함된 서비스 로그인입니다. 이 컴퓨터에 저장되며 웹, 데스크톱, 페어링된 휴대폰이 공유합니다.',
|
'settings.integrations.firstParty.info': 'OpenChamber에 포함된 서비스 로그인입니다. 이 컴퓨터에 저장되며 웹, 데스크톱, 페어링된 휴대폰이 공유합니다.',
|
||||||
'settings.integrations.linear.title': 'Linear',
|
'settings.integrations.linear.title': 'Linear',
|
||||||
@@ -283,6 +301,9 @@ export const linearIntegrationI18n = {
|
|||||||
'settings.magicPrompts.page.group.linearIssueReview.description': 'Linear 이슈로 세션을 시작할 때 쓰는 프롬프트입니다: 표시 사용자 메시지 + 숨겨진 지침.',
|
'settings.magicPrompts.page.group.linearIssueReview.description': 'Linear 이슈로 세션을 시작할 때 쓰는 프롬프트입니다: 표시 사용자 메시지 + 숨겨진 지침.',
|
||||||
},
|
},
|
||||||
pl: {
|
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.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.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',
|
'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.',
|
'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': {
|
'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.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.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',
|
'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.',
|
'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: {
|
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.title': 'Вбудовані інтеграції',
|
||||||
'settings.integrations.firstParty.info': 'Входи до сервісів, що входять до OpenChamber. Логін лишається на цьому комп’ютері, тож веб, десктоп і спарений телефон користуються одним обліковим записом.',
|
'settings.integrations.firstParty.info': 'Входи до сервісів, що входять до OpenChamber. Логін лишається на цьому комп’ютері, тож веб, десктоп і спарений телефон користуються одним обліковим записом.',
|
||||||
'settings.integrations.linear.title': 'Linear',
|
'settings.integrations.linear.title': 'Linear',
|
||||||
@@ -424,6 +451,9 @@ export const linearIntegrationI18n = {
|
|||||||
'settings.magicPrompts.page.group.linearIssueReview.description': 'Промпти для старту сесії з Linear issue: видиме повідомлення користувача та приховані інструкції.',
|
'settings.magicPrompts.page.group.linearIssueReview.description': 'Промпти для старту сесії з Linear issue: видиме повідомлення користувача та приховані інструкції.',
|
||||||
},
|
},
|
||||||
'zh-CN': {
|
'zh-CN': {
|
||||||
|
'settings.integrations.github.title': 'GitHub',
|
||||||
|
'settings.integrations.github.description': '连接 GitHub 账号以处理拉取请求和议题。',
|
||||||
|
'settings.integrations.github.status.notConnected': '未连接',
|
||||||
'settings.integrations.firstParty.title': '内置集成',
|
'settings.integrations.firstParty.title': '内置集成',
|
||||||
'settings.integrations.firstParty.info': 'OpenChamber 自带服务的登录。登录保存在这台电脑上,网页、桌面应用和已配对的手机会共用它。',
|
'settings.integrations.firstParty.info': 'OpenChamber 自带服务的登录。登录保存在这台电脑上,网页、桌面应用和已配对的手机会共用它。',
|
||||||
'settings.integrations.linear.title': 'Linear',
|
'settings.integrations.linear.title': 'Linear',
|
||||||
@@ -471,6 +501,9 @@ export const linearIntegrationI18n = {
|
|||||||
'settings.magicPrompts.page.group.linearIssueReview.description': '从 Linear Issue 开始会话时使用的提示词:可见用户消息 + 隐藏指令。',
|
'settings.magicPrompts.page.group.linearIssueReview.description': '从 Linear Issue 开始会话时使用的提示词:可见用户消息 + 隐藏指令。',
|
||||||
},
|
},
|
||||||
'zh-TW': {
|
'zh-TW': {
|
||||||
|
'settings.integrations.github.title': 'GitHub',
|
||||||
|
'settings.integrations.github.description': '連接 GitHub 帳號以處理拉取請求與議題。',
|
||||||
|
'settings.integrations.github.status.notConnected': '未連接',
|
||||||
'settings.integrations.firstParty.title': '內建整合',
|
'settings.integrations.firstParty.title': '內建整合',
|
||||||
'settings.integrations.firstParty.info': 'OpenChamber 內建服務的登入。登入保存在這台電腦上,網頁、桌面應用程式和已配對的手機會共用它。',
|
'settings.integrations.firstParty.info': 'OpenChamber 內建服務的登入。登入保存在這台電腦上,網頁、桌面應用程式和已配對的手機會共用它。',
|
||||||
'settings.integrations.linear.title': 'Linear',
|
'settings.integrations.linear.title': 'Linear',
|
||||||
@@ -518,6 +551,9 @@ export const linearIntegrationI18n = {
|
|||||||
'settings.magicPrompts.page.group.linearIssueReview.description': '從 Linear Issue 開始會話時使用的提示詞:可見使用者訊息 + 隱藏指令。',
|
'settings.magicPrompts.page.group.linearIssueReview.description': '從 Linear Issue 開始會話時使用的提示詞:可見使用者訊息 + 隱藏指令。',
|
||||||
},
|
},
|
||||||
tr: {
|
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.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.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',
|
'settings.integrations.linear.title': 'Linear',
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export const SETTINGS_PAGE_METADATA: readonly SettingsPageMeta[] = [
|
|||||||
title: 'Git',
|
title: 'Git',
|
||||||
group: 'projects',
|
group: 'projects',
|
||||||
kind: 'single',
|
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,
|
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: '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: '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: '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;
|
] as const;
|
||||||
|
|
||||||
const LEGACY_SIDEBAR_SECTION_TO_SETTINGS_SLUG: Record<SidebarSection, SettingsPageSlug> = {
|
const LEGACY_SIDEBAR_SECTION_TO_SETTINGS_SLUG: Record<SidebarSection, SettingsPageSlug> = {
|
||||||
|
|||||||
@@ -527,12 +527,6 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
|
|||||||
// user to an empty spot on the page.
|
// user to an empty spot on the page.
|
||||||
isAvailable: (ctx) => !ctx.isVSCode && useUIStore.getState().agentMemoryFeatureAvailable,
|
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',
|
id: 'git.identities',
|
||||||
page: 'git',
|
page: 'git',
|
||||||
@@ -989,7 +983,15 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
|
|||||||
page: 'integrations',
|
page: 'integrations',
|
||||||
titleKey: 'settings.integrations.firstParty.title',
|
titleKey: 'settings.integrations.firstParty.title',
|
||||||
descriptionKey: 'settings.integrations.firstParty.info',
|
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,
|
isAvailable: (ctx) => !ctx.isVSCode,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ edge (`components/layout/ContextPanelRail.tsx`) and rendered by
|
|||||||
configure button — `ContextRailSurfacesDialog`), drops the plan surface
|
configure button — `ContextRailSurfacesDialog`), drops the plan surface
|
||||||
unless plan mode is enabled,
|
unless plan mode is enabled,
|
||||||
drops the walkthrough on VS Code and below `WALKTHROUGH_MIN_WIDTH`, hides
|
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
|
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.
|
on a rail badge always maps to the same surface the shortcut opens.
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const baseOptions = {
|
|||||||
screenWidth: 1200,
|
screenWidth: 1200,
|
||||||
tabs: [],
|
tabs: [],
|
||||||
linearConnected: true,
|
linearConnected: true,
|
||||||
|
githubConnected: true,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
describe('getVisibleContextRailSurfaces', () => {
|
describe('getVisibleContextRailSurfaces', () => {
|
||||||
@@ -65,12 +66,16 @@ describe('getVisibleContextRailSurfaces', () => {
|
|||||||
expect(surfaces.slice(0, 2).map((surface) => surface.id)).toEqual(['git', 'context']);
|
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 ids = getVisibleContextRailSurfaces(baseOptions).map((surface) => surface.id);
|
||||||
const pr = ids.indexOf('pr');
|
const walkthrough = ids.indexOf('walkthrough');
|
||||||
const linear = ids.indexOf('linear');
|
expect(walkthrough).toBeGreaterThanOrEqual(0);
|
||||||
expect(pr).toBeGreaterThanOrEqual(0);
|
expect(ids.indexOf('linear')).toBe(walkthrough + 1);
|
||||||
expect(linear).toBe(pr + 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', () => {
|
test('hides Linear until a workspace is connected', () => {
|
||||||
|
|||||||
@@ -66,15 +66,6 @@ export const CONTEXT_SURFACES: readonly ContextSurfaceDescriptor[] = [
|
|||||||
labelKey: 'contextPanel.mode.pr',
|
labelKey: 'contextPanel.mode.pr',
|
||||||
availability: 'always',
|
availability: 'always',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'linear',
|
|
||||||
descriptionKey: 'contextRail.surface.linear.description',
|
|
||||||
defaultWidthFraction: 0.45,
|
|
||||||
mode: 'linear',
|
|
||||||
icon: 'linear',
|
|
||||||
labelKey: 'contextPanel.mode.linear',
|
|
||||||
availability: 'always',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'diff',
|
id: 'diff',
|
||||||
descriptionKey: 'contextRail.surface.diff.description',
|
descriptionKey: 'contextRail.surface.diff.description',
|
||||||
@@ -93,6 +84,15 @@ export const CONTEXT_SURFACES: readonly ContextSurfaceDescriptor[] = [
|
|||||||
labelKey: 'contextPanel.mode.walkthrough',
|
labelKey: 'contextPanel.mode.walkthrough',
|
||||||
availability: 'always',
|
availability: 'always',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'linear',
|
||||||
|
descriptionKey: 'contextRail.surface.linear.description',
|
||||||
|
defaultWidthFraction: 0.45,
|
||||||
|
mode: 'linear',
|
||||||
|
icon: 'linear',
|
||||||
|
labelKey: 'contextPanel.mode.linear',
|
||||||
|
availability: 'always',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'editor',
|
id: 'editor',
|
||||||
descriptionKey: 'contextRail.surface.editor.description',
|
descriptionKey: 'contextRail.surface.editor.description',
|
||||||
@@ -206,6 +206,10 @@ type VisibleRailSurfacesOptions = {
|
|||||||
tabs: readonly { mode: ContextPanelMode }[];
|
tabs: readonly { mode: ContextPanelMode }[];
|
||||||
/** Linear's rail icon stays off until a workspace is connected. */
|
/** Linear's rail icon stays off until a workspace is connected. */
|
||||||
linearConnected: boolean;
|
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) {
|
if (surface.id === 'linear' && !options.linearConnected) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (surface.id === 'pr' && !options.githubConnected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (surface.availability === 'has-content') {
|
if (surface.availability === 'has-content') {
|
||||||
return options.tabs.some((tab) => tab.mode === surface.mode);
|
return options.tabs.some((tab) => tab.mode === surface.mode);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user