feat(ui): show GitLab merge request status in walkthrough, git view and work status
This commit is contained in:
@@ -4,6 +4,8 @@ import { useGitStore } from '@/stores/useGitStore';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import { runBackgroundNetworkTask } from '@/lib/background-network';
|
||||
import { getGitHubPrStatusKey, usePrVisualSummary } from '@/stores/useGitHubPrStatusStore';
|
||||
import { useGitLabMrForBranch } from '@/lib/gitlabMrStatus';
|
||||
import { useGitProvider } from '@/lib/gitProvider';
|
||||
import { useSession, useSessionMessages } from '@/sync/sync-context';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
@@ -113,6 +115,12 @@ export const WorkStatusPrimaryGroup: React.FC<Props> = ({ sessionId, directory,
|
||||
);
|
||||
const prSummary = usePrVisualSummary(prKey);
|
||||
|
||||
// GitLab merge requests ride the same shared TTL cache as the git view and
|
||||
// the walkthrough, so every surface that reports the branch's request stays
|
||||
// consistent without extra requests.
|
||||
const gitProvider = useGitProvider(directory);
|
||||
const { mr: gitLabMr } = useGitLabMrForBranch(directory, branch);
|
||||
|
||||
// `getCurrentModel` is an imperative getter: its reference never changes, so
|
||||
// calling it in render subscribes to nothing. Subscribe to the selected model
|
||||
// ids and recompute the limits from those.
|
||||
@@ -201,7 +209,17 @@ export const WorkStatusPrimaryGroup: React.FC<Props> = ({ sessionId, directory,
|
||||
|
||||
const cost = typeof session?.cost === 'number' && session.cost > 0 ? session.cost : null;
|
||||
const hasSession = showSession && (usagePercent !== null || cost !== null || Boolean(goalRow));
|
||||
const hasRepository = showRepository && Boolean(branch || changed || prSummary || attentionLabel);
|
||||
const hasGitLabMr = gitProvider === 'gitlab' && gitLabMr !== null;
|
||||
const gitLabMrVisualState = gitLabMr
|
||||
? gitLabMr.state === 'merged'
|
||||
? 'merged'
|
||||
: gitLabMr.state === 'closed'
|
||||
? 'closed'
|
||||
: gitLabMr.draft
|
||||
? 'draft'
|
||||
: 'open'
|
||||
: null;
|
||||
const hasRepository = showRepository && Boolean(branch || changed || prSummary || attentionLabel || hasGitLabMr);
|
||||
|
||||
useReportWorkStatusPresence('session-repository', hasSession || hasRepository);
|
||||
|
||||
@@ -284,6 +302,24 @@ export const WorkStatusPrimaryGroup: React.FC<Props> = ({ sessionId, directory,
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{hasGitLabMr && gitLabMr ? (
|
||||
<WorkStatusRow
|
||||
icon="git-merge"
|
||||
onClick={directory ? () => openSurface('pr') : undefined}
|
||||
ariaLabel={t('chat.workStatus.action.openMr')}
|
||||
iconColor={`var(--pr-${gitLabMrVisualState})`}
|
||||
label={gitLabMr.title || t('chat.workStatus.mr.untitled')}
|
||||
value={(
|
||||
<WorkStatusPill
|
||||
color={`var(--pr-${gitLabMrVisualState})`}
|
||||
background={`color-mix(in srgb, var(--pr-${gitLabMrVisualState}) 18%, transparent)`}
|
||||
>
|
||||
{gitLabMr.draft ? t('chat.workStatus.pr.draft') : `!${gitLabMr.number}`}
|
||||
</WorkStatusPill>
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{prSummary ? (
|
||||
<>
|
||||
<WorkStatusRow
|
||||
|
||||
@@ -59,6 +59,7 @@ import { InProgressOperationBanner } from './git/InProgressOperationBanner';
|
||||
import { BranchIntegrationSection, type OperationLogEntry } from './git/BranchIntegrationSection';
|
||||
import { deriveBaseBranch } from './git/baseBranch';
|
||||
import { getFreshestPrStatusForBranch, useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore';
|
||||
import { useGitLabMrForBranch } from '@/lib/gitlabMrStatus';
|
||||
import { createGitIndexMutationQueue, type GitIndexMutationDirection, type GitIndexMutationQueue } from './git/gitIndexMutationQueue';
|
||||
import type { GitRemote } from '@/lib/gitApi';
|
||||
import { getRootBranch } from '@/lib/worktrees/worktreeStatus';
|
||||
@@ -304,6 +305,7 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
|
||||
const openContextSurface = useUIStore((state) => state.openContextSurface);
|
||||
|
||||
const prStatusBranch = status?.current ?? null;
|
||||
const { mr: gitLabMr } = useGitLabMrForBranch(currentDirectory, prStatusBranch);
|
||||
const prChipStatus = useGitHubPrStatusStore((state) => {
|
||||
if (!currentDirectory || !prStatusBranch) {
|
||||
return null;
|
||||
@@ -2361,6 +2363,10 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
|
||||
onOpenPullRequest={
|
||||
currentDirectory ? () => openContextSurface(currentDirectory, 'pr') : undefined
|
||||
}
|
||||
gitLabMr={gitLabMr}
|
||||
onOpenGitLabMr={
|
||||
currentDirectory ? () => openContextSurface(currentDirectory, 'pr') : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{/* In-progress operation banner */}
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
GitRemoteComparison,
|
||||
GitHubPullRequest,
|
||||
GitHubChecksSummary,
|
||||
GitLabMergeRequestSummary,
|
||||
} from '@/lib/api/types';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
@@ -51,6 +52,8 @@ interface GitHeaderProps {
|
||||
pullRequest?: GitHubPullRequest | null;
|
||||
prChecks?: GitHubChecksSummary | null;
|
||||
onOpenPullRequest?: () => void;
|
||||
gitLabMr?: GitLabMergeRequestSummary | null;
|
||||
onOpenGitLabMr?: () => void;
|
||||
}
|
||||
|
||||
const IDENTITY_ICON_MAP: Record<string, IconName> = {
|
||||
@@ -258,6 +261,8 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
|
||||
pullRequest,
|
||||
prChecks,
|
||||
onOpenPullRequest,
|
||||
gitLabMr,
|
||||
onOpenGitLabMr,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
if (!status) {
|
||||
@@ -371,6 +376,40 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
|
||||
</Tooltip>
|
||||
) : null;
|
||||
|
||||
// GitLab merge request chip, mirroring the GitHub PR chip above. GitLab
|
||||
// states are surfaced with the same PR state palette so merged/closed/open
|
||||
// read identically across providers.
|
||||
const gitLabMrVisualState = gitLabMr
|
||||
? gitLabMr.state === 'merged'
|
||||
? 'merged'
|
||||
: gitLabMr.state === 'closed'
|
||||
? 'closed'
|
||||
: gitLabMr.draft
|
||||
? 'draft'
|
||||
: 'open'
|
||||
: null;
|
||||
|
||||
const gitLabMrChip = gitLabMr && onOpenGitLabMr ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onOpenGitLabMr}
|
||||
className="h-8 gap-1.5 px-2 typography-micro"
|
||||
>
|
||||
<Icon
|
||||
name="git-merge"
|
||||
className="size-3.5"
|
||||
style={{ color: `var(--pr-${gitLabMrVisualState})` }}
|
||||
/>
|
||||
<span className="tabular-nums text-foreground/80">!{gitLabMr.number}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8}>{t('gitView.header.openMergeRequest')}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null;
|
||||
|
||||
const syncButtons = (
|
||||
<SyncActions
|
||||
syncAction={syncAction}
|
||||
@@ -435,6 +474,7 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
|
||||
|
||||
<div className="mt-3 flex h-8 min-w-0 items-center gap-2">
|
||||
{prChip ? <div className="shrink-0">{prChip}</div> : null}
|
||||
{gitLabMrChip ? <div className="shrink-0">{gitLabMrChip}</div> : null}
|
||||
<div className="min-w-0 flex-1" />
|
||||
{upstreamStatusPill ? (
|
||||
<div className="min-w-0 shrink">{upstreamStatusPill}</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
|
||||
import { useI18n, type Locale } from '@/lib/i18n';
|
||||
import { openExternalUrl } from '@/lib/url';
|
||||
import { useGitProvider } from '@/lib/gitProvider';
|
||||
import { useGitLabMrForBranch } from '@/lib/gitlabMrStatus';
|
||||
import { buildWalkthroughView } from '@/lib/walkthrough/model';
|
||||
import type { WalkthroughSource, WalkthroughWorkingTreeScope } from '@/lib/walkthrough/types';
|
||||
import { ModelSelector } from '@/components/sections/agents/ModelSelector';
|
||||
@@ -210,6 +211,7 @@ export const WalkthroughView = ({ directory }: WalkthroughViewProps) => {
|
||||
const setPrStatusParams = useGitHubPrStatusStore((state) => state.setParams);
|
||||
const refreshPrStatusTargets = useGitHubPrStatusStore((state) => state.refreshTargets);
|
||||
const gitProvider = useGitProvider(directory);
|
||||
const gitLabMr = useGitLabMrForBranch(directory, currentBranch);
|
||||
|
||||
useEffect(() => {
|
||||
if (!directory || !currentBranch || !githubAuthChecked || !githubConnected || gitProvider !== 'github') return;
|
||||
@@ -250,12 +252,18 @@ export const WalkthroughView = ({ directory }: WalkthroughViewProps) => {
|
||||
[requestedSource, scope]
|
||||
);
|
||||
|
||||
// Offer whichever pull request we know about: the one already selected, or
|
||||
// the one this branch has.
|
||||
// Offer whichever pull request or merge request we know about: the one
|
||||
// already selected, or the one this branch has. GitLab repos get their MR
|
||||
// number from the branch lookup; everything else falls back to the GitHub PR
|
||||
// status store, which the polling effect above only fills for GitHub repos.
|
||||
const prSource = useMemo<Extract<WalkthroughSource, { kind: 'pr' }> | null>(() => {
|
||||
if (source.kind === 'pr') return source;
|
||||
if (gitProvider === 'gitlab') {
|
||||
const number = gitLabMr.mr?.number;
|
||||
return number ? { kind: 'pr', number } : null;
|
||||
}
|
||||
return branchPrNumber ? { kind: 'pr', number: branchPrNumber } : null;
|
||||
}, [branchPrNumber, source]);
|
||||
}, [branchPrNumber, gitLabMr.mr, gitProvider, source]);
|
||||
|
||||
const selectWorkingTree = useCallback(
|
||||
(value: WalkthroughWorkingTreeScope) => {
|
||||
@@ -341,7 +349,9 @@ export const WalkthroughView = ({ directory }: WalkthroughViewProps) => {
|
||||
const sourceLabel = source.kind === 'branch'
|
||||
? t('walkthrough.scope.branch')
|
||||
: source.kind === 'pr'
|
||||
? t('walkthrough.scope.pullRequest', { number: source.number })
|
||||
? gitProvider === 'gitlab'
|
||||
? t('walkthrough.scope.mergeRequest', { number: source.number })
|
||||
: t('walkthrough.scope.pullRequest', { number: source.number })
|
||||
: scope === 'all'
|
||||
? t('walkthrough.scope.all')
|
||||
: scope === 'staged'
|
||||
@@ -547,7 +557,9 @@ export const WalkthroughView = ({ directory }: WalkthroughViewProps) => {
|
||||
)}
|
||||
{prSource && (
|
||||
<DropdownMenuRadioItem value="pr">
|
||||
{t('walkthrough.scope.pullRequest', { number: prSource.number })}
|
||||
{gitProvider === 'gitlab'
|
||||
? t('walkthrough.scope.mergeRequest', { number: prSource.number })
|
||||
: t('walkthrough.scope.pullRequest', { number: prSource.number })}
|
||||
</DropdownMenuRadioItem>
|
||||
)}
|
||||
</DropdownMenuRadioGroup>
|
||||
|
||||
Reference in New Issue
Block a user