From 7492a35695378a12c45ead901e4b4c585e059973 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:10:46 +0200 Subject: [PATCH 1/4] feat: Show the Conversation Pull Request in the Chat Header A header chip shows the pull request a code conversation opened: the agent picture with a CI status dot, the pull request icon and a truncated title. It opens a card with the number, added and removed lines, a link to GitHub, and state and checks badges. The chip renders nothing while loading, when there is no pull request, or when the first lookup fails. --- client/src/components/Chat/Header.tsx | 4 + .../src/components/Chat/PullRequest/Card.tsx | 83 +++++++++ .../src/components/Chat/PullRequest/Chip.tsx | 125 +++++++++++++ .../src/components/Chat/PullRequest/Icon.tsx | 25 +++ .../Chat/PullRequest/__tests__/Card.test.tsx | 60 ++++++ .../Chat/PullRequest/__tests__/Chip.test.tsx | 173 ++++++++++++++++++ .../Chat/PullRequest/__tests__/status.test.ts | 89 +++++++++ .../src/components/Chat/PullRequest/index.ts | 1 + .../src/components/Chat/PullRequest/status.ts | 104 +++++++++++ .../Chat/__tests__/Header.stacking.spec.tsx | 15 ++ client/src/data-provider/PullRequest/index.ts | 1 + .../PullRequest/queries.test.tsx | 83 +++++++++ .../src/data-provider/PullRequest/queries.ts | 41 +++++ client/src/data-provider/index.ts | 1 + client/src/locales/en/translation.json | 17 ++ 15 files changed, 822 insertions(+) create mode 100644 client/src/components/Chat/PullRequest/Card.tsx create mode 100644 client/src/components/Chat/PullRequest/Chip.tsx create mode 100644 client/src/components/Chat/PullRequest/Icon.tsx create mode 100644 client/src/components/Chat/PullRequest/__tests__/Card.test.tsx create mode 100644 client/src/components/Chat/PullRequest/__tests__/Chip.test.tsx create mode 100644 client/src/components/Chat/PullRequest/__tests__/status.test.ts create mode 100644 client/src/components/Chat/PullRequest/index.ts create mode 100644 client/src/components/Chat/PullRequest/status.ts create mode 100644 client/src/data-provider/PullRequest/index.ts create mode 100644 client/src/data-provider/PullRequest/queries.test.tsx create mode 100644 client/src/data-provider/PullRequest/queries.ts diff --git a/client/src/components/Chat/Header.tsx b/client/src/components/Chat/Header.tsx index 45fd682b1a4..8f56752b4db 100644 --- a/client/src/components/Chat/Header.tsx +++ b/client/src/components/Chat/Header.tsx @@ -17,6 +17,7 @@ import { TraceButton, useTraceControl } from './Trace'; import { useGetStartupConfig } from '~/data-provider'; import ExportAndShareMenu from './ExportAndShareMenu'; import SubagentThreadLink from './SubagentThreadLink'; +import { PullRequestChip } from './PullRequest'; import BookmarkMenu from './Menus/BookmarkMenu'; import AddMultiConvo from './AddMultiConvo'; import { useHasAccess } from '~/hooks'; @@ -117,6 +118,9 @@ function Header({
{showTemporaryChat && } {!isNewChat && } + {!isNewChat && parentConversationId == null && ( + + )} {!isNewChat && parentConversationId == null && ( void; +}; + +function PullRequestCard({ pullRequest, refreshFailed = false, onRetry }: CardProps) { + const localize = useLocalize(); + const view = presentPullRequest(pullRequest); + const openLabel = localize('com_ui_pr_open_in_github'); + + return ( +
+
+ + + {localize('com_ui_pr_label', { 0: pullRequest.number })} + + + + +{pullRequest.additions} + + + -{pullRequest.deletions} + + + + + + } + /> +
+

{pullRequest.title}

+
+ + {localize('com_ui_pr_state', { 0: localize(view.stateKey) })} + + + {localize('com_ui_pr_checks', { 0: localize(view.checksKey) })} + +
+ {refreshFailed && ( +
+ {localize('com_ui_pr_refresh_failed')} + {onRetry != null && ( + + )} +
+ )} +
+ ); +} + +export default memo(PullRequestCard); diff --git a/client/src/components/Chat/PullRequest/Chip.tsx b/client/src/components/Chat/PullRequest/Chip.tsx new file mode 100644 index 00000000000..55dcc8a85ec --- /dev/null +++ b/client/src/components/Chat/PullRequest/Chip.tsx @@ -0,0 +1,125 @@ +import { memo, useRef } from 'react'; +import * as Ariakit from '@ariakit/react'; +import { TooltipAnchor } from '@librechat/client'; +import type { TConversationPullRequest } from 'librechat-data-provider'; +import { useConversationPullRequestQuery } from '~/data-provider'; +import { useAgentsMapContext, useChatContext } from '~/Providers'; +import { TONE_DOT_CLASS, presentPullRequest } from './status'; +import { URLIcon } from '~/components/Endpoints/URLIcon'; +import { useLocalize } from '~/hooks'; +import PullRequestIcon from './Icon'; +import PullRequestCard from './Card'; +import { cn } from '~/utils'; + +function CiDot({ dotClass }: { dotClass: string }) { + return ( + + ); +} + +/** The agent's picture with the CI dot on its corner. */ +function AgentAvatar({ + avatar, + name, + dotClass, +}: { + avatar: string; + name?: string | null; + dotClass: string | null; +}) { + return ( + + ); +} + +function summaryLabel( + pr: TConversationPullRequest, + localize: ReturnType, +): string { + const view = presentPullRequest(pr); + return [ + `${localize('com_ui_pull_request')} ${localize('com_ui_pr_label', { 0: pr.number })}: ${pr.title}`, + localize('com_ui_pr_state', { 0: localize(view.stateKey) }), + localize('com_ui_pr_checks', { 0: localize(view.checksKey) }), + ].join(', '); +} + +/** + * Header control for the pull request a code conversation opened. It renders nothing until a + * pull request is known: a conversation without one, a disabled feature and a failed first + * lookup all leave the header exactly as it was, so the row never shifts. + */ +function PullRequestChip({ conversationId }: { conversationId: string }) { + const localize = useLocalize(); + const { conversation } = useChatContext(); + const agentsMap = useAgentsMapContext(); + const popover = Ariakit.usePopoverStore({ placement: 'bottom-end' }); + const disclosureRef = useRef(null); + const { data, isError, refetch } = useConversationPullRequestQuery(conversationId); + const pullRequest = data?.pullRequest; + + if (pullRequest == null) return null; + + const view = presentPullRequest(pullRequest); + const label = summaryLabel(pullRequest, localize); + const dotClass = view.dotTone == null ? null : TONE_DOT_CLASS[view.dotTone]; + const agentId = conversation?.agent_id; + const agent = agentId == null ? undefined : agentsMap?.[agentId]; + const avatar = agent?.avatar?.filepath ?? ''; + const hasAvatar = avatar !== ''; + + return ( + <> + + {hasAvatar && } + + + {!hasAvatar && dotClass != null && } + + {pullRequest.title} + + } + /> + + void refetch()} + /> + + + ); +} + +export default memo(PullRequestChip); diff --git a/client/src/components/Chat/PullRequest/Icon.tsx b/client/src/components/Chat/PullRequest/Icon.tsx new file mode 100644 index 00000000000..9373e31a7b1 --- /dev/null +++ b/client/src/components/Chat/PullRequest/Icon.tsx @@ -0,0 +1,25 @@ +import { GitMerge, GitPullRequest, GitPullRequestClosed, GitPullRequestDraft } from 'lucide-react'; +import type { PullRequestIcon as IconName, PullRequestTone } from './status'; +import { TONE_ICON_CLASS } from './status'; +import { cn } from '~/utils'; + +const ICONS = { + open: GitPullRequest, + draft: GitPullRequestDraft, + merged: GitMerge, + closed: GitPullRequestClosed, +} as const; + +/** Decorative: the surrounding label already says what state the pull request is in. */ +export default function PullRequestIcon({ + icon, + tone, + className, +}: { + icon: IconName; + tone: PullRequestTone; + className?: string; +}) { + const Icon = ICONS[icon]; + return