diff --git a/ui/src/components/ExternalObjectPill.test.tsx b/ui/src/components/ExternalObjectPill.test.tsx index be9325903ca..4d5201f7738 100644 --- a/ui/src/components/ExternalObjectPill.test.tsx +++ b/ui/src/components/ExternalObjectPill.test.tsx @@ -91,7 +91,7 @@ describe("ExternalObjectPill", () => { expect(html).not.toContain("×"); }); - it("uses provider display keys, icons, visible status, and a truncated title when supplied", () => { + it("uses the object link label, provider icon, and visible status when supplied", () => { const html = renderToStaticMarkup( { }} />, ); - expect(html).toContain("Github Pull Request"); expect(html).toContain("Merged"); - expect(html).toContain("acme/web#241: Add rich object presentation metadata"); + expect(html).toContain("acme/web#241"); + expect(html).toContain("text-violet-600"); + expect(html).not.toContain("Github Pull Request"); expect(html).toContain('aria-label="GitHub pull request — Merged: acme/web#241: Add rich object presentation metadata"'); }); }); diff --git a/ui/src/components/ExternalObjectPill.tsx b/ui/src/components/ExternalObjectPill.tsx index d473806c476..7e760e686df 100644 --- a/ui/src/components/ExternalObjectPill.tsx +++ b/ui/src/components/ExternalObjectPill.tsx @@ -31,6 +31,32 @@ export interface ExternalObjectPillData { url?: string | null; } +function githubObjectLabel(url: string | null | undefined): string | null { + if (!url) return null; + try { + const parsed = new URL(url); + if (parsed.hostname !== "github.com") return null; + const [, owner, repo, kind, number] = parsed.pathname.split("/"); + if (!owner || !repo || !number) return null; + if (kind !== "pull" && kind !== "issues") return null; + return `${owner}/${repo}#${number}`; + } catch { + return null; + } +} + +function externalObjectValueLabel(object: ExternalObjectPillData, fallback: string): string { + const githubLabel = object.providerKey === "github" ? githubObjectLabel(object.url) : null; + return githubLabel ?? object.displayTitle?.trim() ?? fallback; +} + +function externalObjectPillTone(object: ExternalObjectPillData): string { + if (object.statusIconKey === "git-merge") { + return "text-violet-600 border-violet-600 dark:text-violet-400 dark:border-violet-400"; + } + return externalObjectStatusIcon[object.statusCategory] ?? externalObjectStatusIconDefault; +} + interface ExternalObjectPillProps { object: ExternalObjectPillData; /** Optional external mention count (renders as `×N` superscript when > 1). */ @@ -59,11 +85,12 @@ export function ExternalObjectPill({ children, inert, }: ExternalObjectPillProps) { - const tone = externalObjectStatusIcon[object.statusCategory] ?? externalObjectStatusIconDefault; + const tone = externalObjectPillTone(object); const overlay = externalObjectLivenessOverlay[object.liveness] ?? ""; const providerLabel = externalObjectProviderLabel(object.providerKey); const typeLabel = externalObjectTypeLabel(object.objectType); const displayKey = object.displayKey?.trim() || `${providerLabel} ${typeLabel}`; + const valueLabel = externalObjectValueLabel(object, displayKey); const statusLabel = object.statusLabel ?? externalObjectCategoryLabel(object.statusCategory); const livenessLabel = externalObjectLivenessLabel(object.liveness); const ProviderIcon = externalObjectIconForKey(object.iconKey); @@ -88,8 +115,7 @@ export function ExternalObjectPill({ : object.displayTitle ?? displayKey; const labelText = children ?? ( <> - {displayKey} - + {statusLabel} - {object.displayTitle ? ( - {object.displayTitle} - ) : null} + {valueLabel} ); const countSuffix = typeof sourceCount === "number" && sourceCount > 1 ? ( diff --git a/ui/src/components/ExternalObjectStatusIcon.tsx b/ui/src/components/ExternalObjectStatusIcon.tsx index 0fb7ca3f376..a6a42de052e 100644 --- a/ui/src/components/ExternalObjectStatusIcon.tsx +++ b/ui/src/components/ExternalObjectStatusIcon.tsx @@ -53,7 +53,9 @@ export function ExternalObjectStatusIcon({ }: ExternalObjectStatusIconProps) { const reducedMotion = usePrefersReducedMotion(); const Icon = externalObjectIconForKey(statusIconKey) ?? externalObjectIconForCategory(category); - const tone = externalObjectStatusIcon[category] ?? externalObjectStatusIconDefault; + const tone = statusIconKey === "git-merge" + ? "text-violet-600 border-violet-600 dark:text-violet-400 dark:border-violet-400" + : externalObjectStatusIcon[category] ?? externalObjectStatusIconDefault; const livenessSuffix = liveness === "fresh" || liveness === "unknown" ? "" : ` (${externalObjectLivenessLabel(liveness)})`; diff --git a/ui/src/components/IssueProperties.test.tsx b/ui/src/components/IssueProperties.test.tsx index 4b1f8454ee0..1ed17650d11 100644 --- a/ui/src/components/IssueProperties.test.tsx +++ b/ui/src/components/IssueProperties.test.tsx @@ -1855,12 +1855,18 @@ describe("IssueProperties", () => { expect(container.textContent).toContain("Github Pull Request"); expect(container.textContent).toContain("Github Issue"); + expect(container.textContent).toContain("acme/web#241"); expect(container.textContent).toContain("Merged"); expect(container.textContent).toContain("Open"); expect(container.textContent).not.toContain("External objects"); const label = Array.from(container.querySelectorAll("span")) .find((span) => span.textContent === "Github Pull Request"); expect(label?.querySelector("svg")).toBeTruthy(); + const pullRequestLink = Array.from(container.querySelectorAll("a")) + .find((anchor) => anchor.getAttribute("href") === "https://github.com/acme/web/pull/241"); + expect(pullRequestLink?.textContent).toContain("acme/web#241"); + expect(pullRequestLink?.textContent).toContain("Merged"); + expect(pullRequestLink?.textContent).not.toContain("Github Pull Request"); act(() => root.unmount()); }); diff --git a/ui/src/components/IssueProperties.tsx b/ui/src/components/IssueProperties.tsx index 632617f0acf..df8f47c7fbb 100644 --- a/ui/src/components/IssueProperties.tsx +++ b/ui/src/components/IssueProperties.tsx @@ -211,9 +211,9 @@ function externalObjectRowLabel(group: IssueExternalObjectGroup): React.ReactNod const displayKey = externalObjectRowDisplayKey(group); const Icon = externalObjectIconForKey(pill.iconKey); return ( - - {Icon ?