diff --git a/apps/mobile/src/components/agents/mobile-session-manager.ts b/apps/mobile/src/components/agents/mobile-session-manager.ts index 72c8e40667..42e05630d0 100644 --- a/apps/mobile/src/components/agents/mobile-session-manager.ts +++ b/apps/mobile/src/components/agents/mobile-session-manager.ts @@ -266,6 +266,7 @@ export function createMobileAgentSessionManager({ isPreparingAsync: Boolean(rs && !rs.preparedAt), prompt: rs?.prompt ?? null, initialMessageId: rs?.initialMessageId ?? null, + associatedPr: sessionResult.associatedPr, }; }, }); diff --git a/apps/web/jest.config.ts b/apps/web/jest.config.ts index cbfc1e367f..8d6dc93d1b 100644 --- a/apps/web/jest.config.ts +++ b/apps/web/jest.config.ts @@ -53,7 +53,7 @@ const config: Config = { ], modulePathIgnorePatterns: ['/../../.worktrees/'], transformIgnorePatterns: [ - 'node_modules/.pnpm/(?!(@octokit|universal-user-agent|before-after-hook|bottleneck|p-limit|yocto-queue))', + 'node_modules/.pnpm/(?!(@octokit|universal-user-agent|universal-github-app-jwt|before-after-hook|bottleneck|p-limit|yocto-queue))', ], // Parallel execution configuration diff --git a/apps/web/package.json b/apps/web/package.json index 7d8a0ca4f8..e62eba1d4e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -74,6 +74,7 @@ "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-progress": "^1.1.8", diff --git a/apps/web/src/components/cloud-agent-next/ChatSidebar.tsx b/apps/web/src/components/cloud-agent-next/ChatSidebar.tsx index 5c922dc5f1..df573cf090 100644 --- a/apps/web/src/components/cloud-agent-next/ChatSidebar.tsx +++ b/apps/web/src/components/cloud-agent-next/ChatSidebar.tsx @@ -19,6 +19,7 @@ import { import { usePathname, useRouter } from 'next/navigation'; import { isToday, isYesterday, startOfDay, differenceInCalendarDays, format } from 'date-fns'; import type { StoredSession } from './types'; +import { SessionPrIndicator } from './SessionPrIndicator'; import { isNewSession } from '@/lib/cloud-agent/session-type'; import { cn } from '@/lib/utils'; import { @@ -199,6 +200,7 @@ function SessionRow({ ) : ( <> {session.prompt} + {shouldReplaceTime ? ( = { + open: 'bg-zinc-500/20 text-zinc-400 hover:bg-zinc-500/25', + merged: 'bg-purple-500/20 text-purple-400 hover:bg-purple-500/25', + closed: 'bg-zinc-500/20 text-zinc-400 hover:bg-zinc-500/25', +}; + +function resolveClasses(state: PrBadgeState, reviewDecision: ReviewDecision | null): string { + if (state === 'open') { + if (reviewDecision === 'approved') + return 'bg-emerald-500/20 text-emerald-400 hover:bg-emerald-500/25'; + if (reviewDecision === 'changes_requested') + return 'bg-amber-500/20 text-amber-400 hover:bg-amber-500/25'; + } + return STATE_CLASSES[state]; +} + +function resolveIcon(state: PrBadgeState, reviewDecision: ReviewDecision | null): LucideIcon { + if (state === 'merged') return GitMerge; + if (state === 'closed') return GitPullRequestClosed; + // open state: use review-decision icon when available + if (reviewDecision === 'approved') return CircleCheck; + if (reviewDecision === 'changes_requested') return CircleX; + return GitPullRequest; +} + +const STATE_ARIA_LABELS: Record = { + open: 'open pull request', + merged: 'merged pull request', + closed: 'closed pull request', +}; + +type PrBadgeProps = { + pr: AssociatedPr; +} & Omit, 'children' | 'aria-label'>; + +/** + * Compact pill that summarizes the PR associated with a session row. + * + * Visual mapping: + * - `open` + approved → emerald + CircleCheck + * - `open` + changes_requested → amber + CircleX + * - `open` + review_required → zinc + GitPullRequest + * - `open` + no decision → zinc + GitPullRequest + * - `merged` → purple + GitMerge + * - `closed` → zinc + GitPullRequestClosed (icon distinguishes from open) + */ +export const PrBadge = forwardRef(function PrBadge( + { pr, className, ...rest }, + ref +) { + const state = normalizePrBadgeState(pr.state); + const Icon = resolveIcon(state, pr.reviewDecision ?? null); + const classes = resolveClasses(state, pr.reviewDecision ?? null); + + return ( + + ); +}); + +/** + * Placeholder shown while the parent has no `associatedPr` field yet (e.g. + * during the first list query render). The fixed width avoids layout shift + * when the badge resolves. + * + * The 300ms animation delay matches the kilocode Agent Manager pattern: brief + * loads never flash a skeleton. + */ +export function PrBadgeSkeleton() { + return ( +