diff --git a/ui/src/components/MarkdownBody.test.tsx b/ui/src/components/MarkdownBody.test.tsx
index bdcfd28393b..1a6d8b8fcf2 100644
--- a/ui/src/components/MarkdownBody.test.tsx
+++ b/ui/src/components/MarkdownBody.test.tsx
@@ -241,6 +241,32 @@ describe("MarkdownBody", () => {
expect(html).not.toContain("paperclip-mention-chip--issue");
});
+ it("holds the quicklook preview until an issue mention is confirmed readable (PAP-16070)", () => {
+ // While the fetch is in flight we don't know if the viewer can read the
+ // issue. The mention stays a clickable link, but the IssueLinkQuicklook
+ // hover preview (a Radix Popover portal) must NOT mount yet: a private
+ // mention 404s straight to the locked chip, and mounting then tearing down
+ // that portal to swap in the chip is the element churn that crashed the
+ // chat transcript renderer.
+ const html = renderMarkdown("Depends on PAP-1271 for the hover state.");
+
+ expect(html).toContain('data-mention-pending="true"');
+ expect(html).toContain('data-mention-kind="issue"');
+ expect(html).toContain("paperclip-markdown-issue-ref");
+ // Clickability is preserved through the loading state.
+ expect(html).toContain('href="/issues/PAP-1271"');
+ expect(html).toContain(">PAP-1271<");
+ });
+
+ it("drops the pending marker once a seeded (readable) issue mention resolves", () => {
+ const html = renderMarkdown("Depends on PAP-1271 for the hover state.", [
+ { identifier: "PAP-1271", status: "done" },
+ ]);
+
+ expect(html).toContain('href="/issues/PAP-1271"');
+ expect(html).not.toContain('data-mention-pending="true"');
+ });
+
it("uses concise issue aria labels until a distinct title is available", () => {
const html = renderMarkdown("Depends on PAP-1271 and PAP-1272.", [
{ identifier: "PAP-1271", status: "done" },
diff --git a/ui/src/components/MarkdownBody.tsx b/ui/src/components/MarkdownBody.tsx
index e7e6cecf5e4..56543006a2b 100644
--- a/ui/src/components/MarkdownBody.tsx
+++ b/ui/src/components/MarkdownBody.tsx
@@ -119,10 +119,25 @@ function MarkdownIssueLink({
const status = data?.status;
const issueLabel = title !== identifier ? `Issue ${identifier}: ${title}` : `Issue ${identifier}`;
+ // Until the fetch settles we don't yet know whether this viewer can read the
+ // issue. Keep the mention a plain link (clickable, styled as today) but hold
+ // off mounting the IssueLinkQuicklook hover preview — a Radix Popover portal —
+ // until `data` confirms the issue is readable.
+ // - Correctness (PAP-16070): a private mention 404s straight to the locked
+ // chip. Mounting the quicklook popover during the loading `` only to
+ // tear the portal down and swap in a plain `` chip on the 404 is the
+ // element churn that crashed the chat transcript's primary renderer (it
+ // fell through to the safe fallback). Loading link → chip stays portal-free.
+ // - Privacy: don't prefetch / hover-preview an issue of unconfirmed
+ // readability. `data-mention-pending` marks the transient state for tests.
+ const pending = !data;
+
return (