Thread externalReferences through issue-scoped markdown surfaces

Extend the Phase 6 plumbing so every `MarkdownBody` rendered inside an
issue context receives `externalObjectsState.markdownReferences`. Comments,
documents, chat, continuation handoffs, interaction details, and run
transcripts now decorate resolved external URLs with the §2 status chip.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-23 00:35:04 +00:00
1 parent aa9e782ea3
commit a2476bb79a
9 files changed
+249 -27

No files matched your search

+60 -2
View File
@@ -9,8 +9,22 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { CommentThread } from "./CommentThread";
vi.mock("./MarkdownBody", () => ({
MarkdownBody: ({ children, className }: { children: ReactNode; className?: string }) => (
<div className={className}>{children}</div>
MarkdownBody: ({
children,
className,
externalReferences,
}: {
children: ReactNode;
className?: string;
externalReferences?: Record<string, unknown>;
}) => (
<div
className={className}
data-testid="markdown-body"
data-external-reference-keys={externalReferences ? Object.keys(externalReferences).join(",") : ""}
>
{children}
</div>
),
}));
@@ -395,4 +409,48 @@ describe("CommentThread", () => {
root.unmount();
});
});
it("passes externalReferences to MarkdownBody for comment bodies", () => {
const root = createRoot(container);
act(() => {
root.render(
<MemoryRouter>
<CommentThread
comments={[{
id: "comment-ref",
companyId: "company-1",
issueId: "issue-1",
authorAgentId: null,
authorUserId: "user-1",
body: "See https://github.com/example/repo/pull/42 for context.",
createdAt: new Date("2026-03-11T11:00:00.000Z"),
updatedAt: new Date("2026-03-11T11:00:00.000Z"),
}]}
externalReferences={{
"https://github.com/example/repo/pull/42": {
providerKey: "github",
objectType: "pull_request",
statusCategory: "open",
liveness: "fresh",
statusLabel: "Open",
displayTitle: "PR #42",
},
}}
onAdd={async () => {}}
/>
</MemoryRouter>,
);
});
const commentRow = container.querySelector("#comment-comment-ref") as HTMLDivElement | null;
expect(commentRow).not.toBeNull();
const markdownBody = commentRow?.querySelector('[data-testid="markdown-body"]') as HTMLElement | null;
expect(markdownBody?.getAttribute("data-external-reference-keys"))
.toContain("https://github.com/example/repo/pull/42");
act(() => {
root.unmount();
});
});
});
+11 -2
View File
@@ -14,7 +14,7 @@ import { ArrowRight, Check, Copy, Paperclip } from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Identity } from "./Identity";
import { InlineEntitySelector, type InlineEntityOption } from "./InlineEntitySelector";
import { MarkdownBody } from "./MarkdownBody";
import { MarkdownBody, type MarkdownExternalReferenceMap } from "./MarkdownBody";
import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "./MarkdownEditor";
import { OutputFeedbackButtons } from "./OutputFeedbackButtons";
import { ApprovalCard } from "./ApprovalCard";
@@ -105,6 +105,7 @@ interface CommentThreadProps {
onInterruptQueued?: (runId: string) => Promise<void>;
interruptingQueuedRunId?: string | null;
composerDisabledReason?: string | null;
externalReferences?: MarkdownExternalReferenceMap;
}
const DRAFT_DEBOUNCE_MS = 800;
@@ -324,6 +325,7 @@ function CommentCard({
voting = false,
highlightCommentId,
queued = false,
externalReferences,
}: {
comment: CommentWithRunMeta;
agentMap?: Map<string, Agent>;
@@ -339,6 +341,7 @@ function CommentCard({
voting?: boolean;
highlightCommentId?: string | null;
queued?: boolean;
externalReferences?: MarkdownExternalReferenceMap;
}) {
const isHighlighted = highlightCommentId === comment.id;
const isPending = comment.clientStatus === "pending";
@@ -412,7 +415,7 @@ function CommentCard({
{isDeleted ? (
<div className="text-sm italic text-muted-foreground">Comment deleted</div>
) : (
<MarkdownBody className="text-sm" softBreaks>{comment.body}</MarkdownBody>
<MarkdownBody className="text-sm" softBreaks externalReferences={externalReferences}>{comment.body}</MarkdownBody>
)}
{companyId && !isPending && !isDeleted ? (
<div className="mt-2 space-y-2">
@@ -575,6 +578,7 @@ const TimelineList = memo(function TimelineList({
onVote,
votingTargetId,
highlightCommentId,
externalReferences,
}: {
timeline: TimelineItem[];
agentMap?: Map<string, Agent>;
@@ -597,6 +601,7 @@ const TimelineList = memo(function TimelineList({
) => Promise<void>;
votingTargetId?: string | null;
highlightCommentId?: string | null;
externalReferences?: MarkdownExternalReferenceMap;
}) {
if (timeline.length === 0) {
return <p className="text-sm text-muted-foreground">No timeline entries yet.</p>;
@@ -718,6 +723,7 @@ const TimelineList = memo(function TimelineList({
onVote={onVote ? (vote, options) => onVote(comment.id, vote, options) : undefined}
voting={votingTargetId === comment.id}
highlightCommentId={highlightCommentId}
externalReferences={externalReferences}
/>
);
})}
@@ -756,6 +762,7 @@ export function CommentThread({
onInterruptQueued,
interruptingQueuedRunId = null,
composerDisabledReason = null,
externalReferences,
}: CommentThreadProps) {
const [body, setBody] = useState("");
const [submitting, setSubmitting] = useState(false);
@@ -974,6 +981,7 @@ export function CommentThread({
votingTargetId={votingTargetId}
highlightCommentId={highlightCommentId}
feedbackTermsUrl={feedbackTermsUrl}
externalReferences={externalReferences}
/>
{liveRunSlot}
@@ -1006,6 +1014,7 @@ export function CommentThread({
projectId={projectId}
highlightCommentId={highlightCommentId}
queued
externalReferences={externalReferences}
/>
))}
</div>
+20 -3
View File
@@ -101,7 +101,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { MarkdownBody } from "./MarkdownBody";
import { MarkdownBody, type MarkdownExternalReferenceMap } from "./MarkdownBody";
import { WorkspaceFileMarkdownBody } from "./WorkspaceFileMarkdownBody";
import { MarkdownEditor, type MentionOption, type MarkdownEditorRef } from "./MarkdownEditor";
import { Identity } from "./Identity";
@@ -217,6 +217,7 @@ interface IssueChatMessageContext {
onUploadImage?: (file: File) => Promise<string>;
issueStatus?: string;
successfulRunHandoff?: SuccessfulRunHandoffState | null;
externalReferences?: MarkdownExternalReferenceMap;
}
const IssueChatCtx = createContext<IssueChatMessageContext>({
@@ -450,6 +451,7 @@ interface IssueChatThreadProps {
* comment is in the loaded set before we scroll to it.
*/
onRefreshLatestComments?: () => Promise<unknown> | void;
externalReferences?: MarkdownExternalReferenceMap;
}
type IssueChatErrorBoundaryProps = {
@@ -457,6 +459,7 @@ type IssueChatErrorBoundaryProps = {
messages: readonly ThreadMessage[];
emptyMessage: string;
variant: "full" | "embedded";
externalReferences?: MarkdownExternalReferenceMap;
children: ReactNode;
};
@@ -491,6 +494,7 @@ class IssueChatErrorBoundary extends Component<IssueChatErrorBoundaryProps, Issu
messages={this.props.messages}
emptyMessage={this.props.emptyMessage}
variant={this.props.variant}
externalReferences={this.props.externalReferences}
/>
);
}
@@ -555,10 +559,12 @@ function IssueChatFallbackThread({
messages,
emptyMessage,
variant,
externalReferences,
}: {
messages: readonly ThreadMessage[];
emptyMessage: string;
variant: "full" | "embedded";
externalReferences?: MarkdownExternalReferenceMap;
}) {
return (
<div className={cn(variant === "embedded" ? "space-y-3" : "space-y-4")}>
@@ -599,7 +605,9 @@ function IssueChatFallbackThread({
</div>
<div className="space-y-2">
{lines.length > 0 ? lines.map((line, index) => (
<MarkdownBody key={`${message.id}:fallback:${index}`}>{line}</MarkdownBody>
<MarkdownBody key={`${message.id}:fallback:${index}`} externalReferences={externalReferences}>
{line}
</MarkdownBody>
)) : (
<p className="text-sm text-muted-foreground">No message content.</p>
)}
@@ -705,7 +713,7 @@ function commentDateLabel(date: Date | string | undefined): string {
}
const IssueChatTextPart = memo(function IssueChatTextPart({ text, recessed, onAccent }: { text: string; recessed?: boolean; onAccent?: boolean }) {
const { onImageClick } = useContext(IssueChatCtx);
const { onImageClick, externalReferences } = useContext(IssueChatCtx);
if (isSuccessfulRunHandoffComment(text)) {
return <SuccessfulRunHandoffCommentCallout text={text} recessed={recessed} onImageClick={onImageClick} />;
}
@@ -717,6 +725,7 @@ const IssueChatTextPart = memo(function IssueChatTextPart({ text, recessed, onAc
style={recessed ? { opacity: 0.55 } : undefined}
softBreaks
onImageClick={onImageClick}
externalReferences={externalReferences}
>
{text}
</WorkspaceFileMarkdownBody>
@@ -2113,6 +2122,7 @@ function ExpiredRequestConfirmationActivity({
onRejectInteraction,
onCancelInteraction,
onUploadImage,
externalReferences,
} = useContext(IssueChatCtx);
const [expanded, setExpanded] = useState(false);
const hasResolvedActor = Boolean(interaction.resolvedByAgentId || interaction.resolvedByUserId);
@@ -2193,6 +2203,7 @@ function ExpiredRequestConfirmationActivity({
onRejectInteraction={onRejectInteraction}
onCancelInteraction={onCancelInteraction}
onUploadImage={onUploadImage}
externalReferences={externalReferences}
/>
</div>
) : null}
@@ -2620,6 +2631,7 @@ function IssueChatSystemMessage({ message }: { message: ThreadMessage }) {
onSubmitInteractionAnswers,
onCancelInteraction,
onUploadImage,
externalReferences,
} = useContext(IssueChatCtx);
const custom = message.metadata.custom as Record<string, unknown>;
const anchorId = typeof custom.anchorId === "string" ? custom.anchorId : undefined;
@@ -2677,6 +2689,7 @@ function IssueChatSystemMessage({ message }: { message: ThreadMessage }) {
onSubmitInteractionAnswers={onSubmitInteractionAnswers}
onCancelInteraction={onCancelInteraction}
onUploadImage={onUploadImage}
externalReferences={externalReferences}
/>
</div>
</div>
@@ -4094,6 +4107,7 @@ export function IssueChatThread({
assigneeUserId = null,
onResumeFromBacklog,
resumeFromBacklogPending = false,
externalReferences,
}: IssueChatThreadProps) {
const location = useLocation();
const lastScrolledHashRef = useRef<string | null>(null);
@@ -4618,6 +4632,7 @@ export function IssueChatThread({
onUploadImage: stableOnUploadImage,
issueStatus,
successfulRunHandoff,
externalReferences,
}),
[
feedbackDataSharingPreference,
@@ -4643,6 +4658,7 @@ export function IssueChatThread({
stableOnUploadImage,
issueStatus,
successfulRunHandoff,
externalReferences,
],
);
@@ -4680,6 +4696,7 @@ export function IssueChatThread({
messages={messages}
emptyMessage={resolvedEmptyMessage}
variant={variant}
externalReferences={externalReferences}
>
<div data-testid="thread-root">
<div
@@ -3,17 +3,19 @@ import type { IssueDocument } from "@paperclipai/shared";
import { ISSUE_CONTINUATION_SUMMARY_DOCUMENT_KEY } from "@paperclipai/shared";
import { Button } from "@/components/ui/button";
import { cn, relativeTime } from "../lib/utils";
import { MarkdownBody } from "./MarkdownBody";
import { MarkdownBody, type MarkdownExternalReferenceMap } from "./MarkdownBody";
import { Check, ChevronDown, ChevronRight, Copy, History } from "lucide-react";
type IssueContinuationHandoffProps = {
document: IssueDocument | null | undefined;
focusSignal?: number;
externalReferences?: MarkdownExternalReferenceMap;
};
export function IssueContinuationHandoff({
document,
focusSignal = 0,
externalReferences,
}: IssueContinuationHandoffProps) {
const [expanded, setExpanded] = useState(false);
const [copied, setCopied] = useState(false);
@@ -91,7 +93,11 @@ export function IssueContinuationHandoff({
</div>
{expanded ? (
<div className="mt-3 rounded-md border border-border bg-background/80 p-3">
<MarkdownBody className="paperclip-edit-in-place-content text-sm leading-6" softBreaks={false}>
<MarkdownBody
className="paperclip-edit-in-place-content text-sm leading-6"
softBreaks={false}
externalReferences={externalReferences}
>
{document.body}
</MarkdownBody>
</div>
@@ -43,8 +43,22 @@ vi.mock("@/lib/router", () => ({
}));
vi.mock("./MarkdownBody", () => ({
MarkdownBody: ({ children, className }: { children: string; className?: string }) => (
<div className={className}>{children}</div>
MarkdownBody: ({
children,
className,
externalReferences,
}: {
children: string;
className?: string;
externalReferences?: Record<string, unknown>;
}) => (
<div
className={className}
data-testid="markdown-body"
data-external-reference-keys={externalReferences ? Object.keys(externalReferences).join(",") : ""}
>
{children}
</div>
),
}));
@@ -684,6 +698,61 @@ describe("IssueDocumentsSection", () => {
queryClient.clear();
});
it("forwards externalReferences to the rendered document body so URL decoration applies", async () => {
const issue = createIssue();
const root = createRoot(container);
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
mockIssuesApi.listDocuments.mockResolvedValue([
createIssueDocument({
body: "Linked work: https://github.com/example/repo/pull/99",
}),
]);
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<IssueDocumentsSection
issue={issue}
canDeleteDocuments={false}
externalReferences={{
"https://github.com/example/repo/pull/99": {
providerKey: "github",
objectType: "pull_request",
statusCategory: "open",
liveness: "fresh",
statusLabel: "Open",
displayTitle: "PR #99",
},
}}
/>
</QueryClientProvider>,
);
});
await flush();
await flush();
const markdownBodies = Array.from(
container.querySelectorAll('[data-testid="markdown-body"]'),
) as HTMLElement[];
expect(markdownBodies.length).toBeGreaterThan(0);
const rendered = markdownBodies.find((element) =>
(element.textContent ?? "").includes("Linked work"),
);
expect(rendered?.getAttribute("data-external-reference-keys"))
.toContain("https://github.com/example/repo/pull/99");
await act(async () => {
root.unmount();
});
queryClient.clear();
});
it("wraps the documents header actions so mobile layouts do not overflow", async () => {
const issue = createIssue();
const root = createRoot(container);
+17 -7
View File
@@ -20,7 +20,7 @@ import { queryKeys } from "../lib/queryKeys";
import { cn, relativeTime } from "../lib/utils";
import { FoldCurtain } from "./FoldCurtain";
import { DocumentAnnotationsCountChip, IssueDocumentAnnotations } from "./IssueDocumentAnnotations";
import { MarkdownBody } from "./MarkdownBody";
import { MarkdownBody, type MarkdownExternalReferenceMap } from "./MarkdownBody";
import { MarkdownEditor, type MentionOption } from "./MarkdownEditor";
import { OutputFeedbackButtons } from "./OutputFeedbackButtons";
import { Button } from "@/components/ui/button";
@@ -75,10 +75,16 @@ function saveFoldedDocumentKeys(issueId: string, keys: string[]) {
window.localStorage.setItem(getFoldedDocumentsStorageKey(issueId), JSON.stringify(keys));
}
function renderFoldableBody(body: string, className?: string) {
function renderFoldableBody(
body: string,
className?: string,
externalReferences?: MarkdownExternalReferenceMap,
) {
return (
<FoldCurtain>
<MarkdownBody className={className} softBreaks={false}>{body}</MarkdownBody>
<MarkdownBody className={className} softBreaks={false} externalReferences={externalReferences}>
{body}
</MarkdownBody>
</FoldCurtain>
);
}
@@ -161,6 +167,7 @@ export function IssueDocumentsSection({
defaultAnnotationPanelOpenKeys,
defaultAnnotationFocusedThreadIds,
forceEditDocumentKey,
externalReferences,
}: {
issue: Issue;
canDeleteDocuments: boolean;
@@ -187,6 +194,7 @@ export function IssueDocumentsSection({
defaultAnnotationFocusedThreadIds?: Readonly<Record<string, string>>;
/** Force a doc into edit mode on mount (Storybook-only). */
forceEditDocumentKey?: string | null;
externalReferences?: MarkdownExternalReferenceMap;
}) {
const queryClient = useQueryClient();
const location = useLocation();
@@ -878,7 +886,9 @@ export function IssueDocumentsSection({
PLAN
</span>
</div>
{renderFoldableBody(issue.legacyPlanDocument.body, documentBodyContentClassName)}
<div className={documentBodyPaddingClassName}>
{renderFoldableBody(issue.legacyPlanDocument.body, documentBodyContentClassName, externalReferences)}
</div>
</div>
) : null}
@@ -1197,7 +1207,7 @@ export function IssueDocumentsSection({
{!isPlanKey(doc.key) && activeConflict.serverDocument.title ? (
<p className="mb-2 text-sm font-medium">{activeConflict.serverDocument.title}</p>
) : null}
{renderFoldableBody(activeConflict.serverDocument.body, "text-[14px] leading-7")}
{renderFoldableBody(activeConflict.serverDocument.body, "text-[14px] leading-7", externalReferences)}
</div>
)}
</div>
@@ -1235,7 +1245,7 @@ export function IssueDocumentsSection({
defaultFocusedThreadId={defaultAnnotationFocusedThreadIds?.[doc.key]}
>
{isHistoricalPreview ? (
renderFoldableBody(displayedBody, documentBodyContentClassName)
renderFoldableBody(displayedBody, documentBodyContentClassName, externalReferences)
) : activeDraft ? (
<MarkdownEditor
value={displayedBody}
@@ -1257,7 +1267,7 @@ export function IssueDocumentsSection({
onSubmit={() => void commitDraft(activeDraft ?? draft, { clearAfterSave: false, trackAutosave: true })}
/>
) : (
renderFoldableBody(displayedBody, documentBodyContentClassName)
renderFoldableBody(displayedBody, documentBodyContentClassName, externalReferences)
)}
</IssueDocumentAnnotations>
</div>
@@ -21,7 +21,7 @@ import {
type SuggestedTaskTreeNode,
} from "../lib/issue-thread-interactions";
import { cn, formatDateTime, formatShortDate } from "../lib/utils";
import { MarkdownBody } from "./MarkdownBody";
import { MarkdownBody, type MarkdownExternalReferenceMap } from "./MarkdownBody";
import { Button } from "./ui/button";
import { Checkbox } from "./ui/checkbox";
import { PriorityIcon } from "./PriorityIcon";
@@ -58,6 +58,7 @@ interface IssueThreadInteractionCardProps {
interaction: AskUserQuestionsInteraction,
) => Promise<void> | void;
onUploadImage?: (file: File) => Promise<string>;
externalReferences?: MarkdownExternalReferenceMap;
}
function resolveActorLabel(args: {
@@ -706,6 +707,7 @@ function AskUserQuestionsCard({
interaction,
onSubmitInteractionAnswers,
onCancelInteraction,
externalReferences,
}: {
interaction: AskUserQuestionsInteraction;
onSubmitInteractionAnswers?: (
@@ -715,6 +717,7 @@ function AskUserQuestionsCard({
onCancelInteraction?: (
interaction: AskUserQuestionsInteraction,
) => Promise<void> | void;
externalReferences?: MarkdownExternalReferenceMap;
}) {
const [draftAnswers, setDraftAnswers] = useState<Record<string, string[]>>(() =>
Object.fromEntries(
@@ -1013,7 +1016,7 @@ function AskUserQuestionsCard({
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-emerald-700">
Submitted summary
</div>
<MarkdownBody>{interaction.result.summaryMarkdown}</MarkdownBody>
<MarkdownBody externalReferences={externalReferences}>{interaction.result.summaryMarkdown}</MarkdownBody>
</div>
) : null}
</div>
@@ -1173,6 +1176,7 @@ function RequestConfirmationCard({
onAcceptInteraction,
onRejectInteraction,
onUploadImage,
externalReferences,
}: {
interaction: RequestConfirmationInteraction;
isPlan?: boolean;
@@ -1184,6 +1188,7 @@ function RequestConfirmationCard({
reason?: string,
) => Promise<void> | void;
onUploadImage?: (file: File) => Promise<string>;
externalReferences?: MarkdownExternalReferenceMap;
}) {
const [rejecting, setRejecting] = useState(false);
const [working, setWorking] = useState<"accept" | "reject" | null>(null);
@@ -1283,7 +1288,7 @@ function RequestConfirmationCard({
</div>
{interaction.payload.detailsMarkdown ? (
<div className="border-t border-border/60 pt-3 text-sm">
<MarkdownBody>{interaction.payload.detailsMarkdown}</MarkdownBody>
<MarkdownBody externalReferences={externalReferences}>{interaction.payload.detailsMarkdown}</MarkdownBody>
</div>
) : null}
<RequestConfirmationTargetChip
@@ -1575,6 +1580,7 @@ function RequestCheckboxConfirmationCard({
interaction,
onAcceptInteraction,
onRejectInteraction,
externalReferences,
}: {
interaction: RequestCheckboxConfirmationInteraction;
onAcceptInteraction?: (
@@ -1586,6 +1592,7 @@ function RequestCheckboxConfirmationCard({
interaction: RequestCheckboxConfirmationInteraction,
reason?: string,
) => Promise<void> | void;
externalReferences?: MarkdownExternalReferenceMap;
}) {
const options = interaction.payload.options;
const optionIds = useMemo(() => options.map((option) => option.id), [options]);
@@ -1721,7 +1728,7 @@ function RequestCheckboxConfirmationCard({
<div className="text-sm leading-6 text-foreground">{interaction.payload.prompt}</div>
{interaction.payload.detailsMarkdown ? (
<div className="border-t border-border/60 pt-3 text-sm">
<MarkdownBody>{interaction.payload.detailsMarkdown}</MarkdownBody>
<MarkdownBody externalReferences={externalReferences}>{interaction.payload.detailsMarkdown}</MarkdownBody>
</div>
) : null}
<RequestConfirmationTargetChip
@@ -1881,6 +1888,7 @@ export function IssueThreadInteractionCard({
onSubmitInteractionAnswers,
onCancelInteraction,
onUploadImage,
externalReferences,
}: IssueThreadInteractionCardProps) {
const isPlan = isPlanConfirmation(interaction);
const planStyles = isPlan ? planStatusClasses(interaction.status) : null;
@@ -1973,12 +1981,14 @@ export function IssueThreadInteractionCard({
interaction={interaction}
onSubmitInteractionAnswers={onSubmitInteractionAnswers}
onCancelInteraction={onCancelInteraction}
externalReferences={externalReferences}
/>
) : interaction.kind === "request_checkbox_confirmation" ? (
<RequestCheckboxConfirmationCard
interaction={interaction}
onAcceptInteraction={onAcceptInteraction}
onRejectInteraction={onRejectInteraction}
externalReferences={externalReferences}
/>
) : (
<RequestConfirmationCard
@@ -1987,6 +1997,8 @@ export function IssueThreadInteractionCard({
onAcceptInteraction={onAcceptInteraction}
onRejectInteraction={onRejectInteraction}
onUploadImage={onUploadImage}
onUploadImage={onUploadImage}
externalReferences={externalReferences}
/>
)}
</div>
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useRef, useState } from "react";
import type { TranscriptEntry } from "../../adapters";
import { MarkdownBody } from "../MarkdownBody";
import { MarkdownBody, type MarkdownExternalReferenceMap } from "../MarkdownBody";
import { cn, formatTokens } from "../../lib/utils";
import {
Check,
@@ -31,6 +31,7 @@ interface RunTranscriptViewProps {
emptyMessage?: string;
className?: string;
thinkingClassName?: string;
externalReferences?: MarkdownExternalReferenceMap;
}
type TranscriptBlock =
@@ -636,9 +637,11 @@ export function normalizeTranscript(entries: TranscriptEntry[], streaming: boole
function TranscriptMessageBlock({
block,
density,
externalReferences,
}: {
block: Extract<TranscriptBlock, { type: "message" }>;
density: TranscriptDensity;
externalReferences?: MarkdownExternalReferenceMap;
}) {
const isAssistant = block.role === "assistant";
const compact = density === "compact";
@@ -656,6 +659,7 @@ function TranscriptMessageBlock({
"[&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
compact ? "text-xs leading-5 text-foreground/85" : "text-sm",
)}
externalReferences={externalReferences}
>
{block.text}
</MarkdownBody>
@@ -676,10 +680,12 @@ function TranscriptThinkingBlock({
block,
density,
className,
externalReferences,
}: {
block: Extract<TranscriptBlock, { type: "thinking" }>;
density: TranscriptDensity;
className?: string;
externalReferences?: MarkdownExternalReferenceMap;
}) {
return (
<MarkdownBody
@@ -688,6 +694,7 @@ function TranscriptThinkingBlock({
density === "compact" ? "text-[11px] leading-5" : "text-sm leading-6",
className,
)}
externalReferences={externalReferences}
>
{block.text}
</MarkdownBody>
@@ -1090,9 +1097,11 @@ function TranscriptActivityRow({
function TranscriptEventRow({
block,
density,
externalReferences,
}: {
block: Extract<TranscriptBlock, { type: "event" }>;
density: TranscriptDensity;
externalReferences?: MarkdownExternalReferenceMap;
}) {
const compact = density === "compact";
const toneClasses =
@@ -1121,6 +1130,7 @@ function TranscriptEventRow({
"[&>*:first-child]:mt-0 [&>*:last-child]:mb-0 text-sky-700 dark:text-sky-300",
compact ? "text-[11px] leading-5" : "text-xs leading-5",
)}
externalReferences={externalReferences}
>
{block.text}
</MarkdownBody>
@@ -1473,6 +1483,7 @@ export function RunTranscriptView({
emptyMessage = "No transcript yet.",
className,
thinkingClassName,
externalReferences,
}: RunTranscriptViewProps) {
const blocks = useMemo(
() => (mode === "raw" ? [] : normalizeTranscript(entries, streaming)),
@@ -1504,9 +1515,20 @@ export function RunTranscriptView({
key={`${block.type}-${block.ts}-${index}`}
className={cn(index === visibleBlocks.length - 1 && streaming && "animate-in fade-in slide-in-from-bottom-1 duration-300")}
>
{block.type === "message" && <TranscriptMessageBlock block={block} density={density} />}
{block.type === "message" && (
<TranscriptMessageBlock
block={block}
density={density}
externalReferences={externalReferences}
/>
)}
{block.type === "thinking" && (
<TranscriptThinkingBlock block={block} density={density} className={thinkingClassName} />
<TranscriptThinkingBlock
block={block}
density={density}
className={thinkingClassName}
externalReferences={externalReferences}
/>
)}
{block.type === "tool" && <TranscriptToolCard block={block} density={density} />}
{block.type === "command_group" && <TranscriptCommandGroup block={block} density={density} />}
@@ -1518,7 +1540,13 @@ export function RunTranscriptView({
<TranscriptStdoutRow block={block} density={density} collapseByDefault={collapseStdout} />
)}
{block.type === "activity" && <TranscriptActivityRow block={block} density={density} />}
{block.type === "event" && <TranscriptEventRow block={block} density={density} />}
{block.type === "event" && (
<TranscriptEventRow
block={block}
density={density}
externalReferences={externalReferences}
/>
)}
</div>
))}
</div>
+14 -1
View File
@@ -79,6 +79,7 @@ import { IssueOutputSection } from "../components/issue-output/IssueOutputSectio
import { isImageAttachment } from "../lib/issue-attachments";
import { getPromotedOutputAttachmentIds } from "../lib/issue-output";
import { IssueSiblingNavigation } from "../components/IssueSiblingNavigation";
import type { MarkdownExternalReferenceMap } from "../components/MarkdownBody";
import { IssuesList } from "../components/IssuesList";
import { AgentIcon } from "../components/AgentIconPicker";
import { IssueReferenceActivitySummary } from "../components/IssueReferenceActivitySummary";
@@ -744,6 +745,7 @@ type IssueDetailChatTabProps = {
assigneeUserId: string | null;
onResumeFromBacklog?: () => Promise<void> | void;
resumeFromBacklogPending?: boolean;
externalReferences?: MarkdownExternalReferenceMap;
};
const IssueDetailChatTab = memo(function IssueDetailChatTab({
@@ -805,6 +807,7 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({
assigneeUserId,
onResumeFromBacklog,
resumeFromBacklogPending,
externalReferences,
}: IssueDetailChatTabProps) {
// Conference Room Chat experimental flag (PAP-136/PAP-139): ON renders the
// NUX thread (bubbles, metadata rows, composer chrome); OFF renders the
@@ -1029,6 +1032,7 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({
onResumeFromBacklog={onResumeFromBacklog}
resumeFromBacklogPending={resumeFromBacklogPending}
footer={footer}
externalReferences={externalReferences}
/>
</div>
);
@@ -1049,6 +1053,7 @@ type IssueDetailActivityTabProps = {
onCheckMonitorNow: () => void;
checkingMonitorNow: boolean;
handoffFocusSignal?: number;
externalReferences?: MarkdownExternalReferenceMap;
};
function IssueDetailActivityTab({
@@ -1066,6 +1071,7 @@ function IssueDetailActivityTab({
onCheckMonitorNow,
checkingMonitorNow,
handoffFocusSignal = 0,
externalReferences,
}: IssueDetailActivityTabProps) {
const { data: activity, isLoading: activityLoading } = useQuery({
queryKey: queryKeys.issues.activity(issueId),
@@ -1267,6 +1273,11 @@ function IssueDetailActivityTab({
}}
/>
</div>
<IssueContinuationHandoff
document={continuationHandoff}
focusSignal={handoffFocusSignal}
externalReferences={externalReferences}
/>
{linkedApprovals && linkedApprovals.length > 0 && (
<div className="mb-3 space-y-3">
{linkedApprovals.map((approval) => (
@@ -1287,7 +1298,6 @@ function IssueDetailActivityTab({
))}
</div>
)}
<IssueContinuationHandoff document={continuationHandoff} focusSignal={handoffFocusSignal} />
<IssueScheduledRetryCard issueId={issue.id} scheduledRetry={issue.scheduledRetry ?? null} />
<IssueMonitorActivityCard
issue={issue}
@@ -4060,6 +4070,7 @@ export function IssueDetail() {
feedbackDataSharingPreference={feedbackDataSharingPreference}
feedbackTermsUrl={FEEDBACK_TERMS_URL}
mentions={mentionOptions}
externalReferences={externalObjectsState.markdownReferences}
imageUploadHandler={async (file) => {
const attachment = await uploadAttachment.mutateAsync(file);
return attachment.contentPath;
@@ -4251,6 +4262,7 @@ export function IssueDetail() {
resumeFromBacklogPending={
updateIssue.isPending && updateIssue.variables?.status === "todo"
}
externalReferences={externalObjectsState.markdownReferences}
/>
) : null}
</TabsContent>
@@ -4274,6 +4286,7 @@ export function IssueDetail() {
}}
onCheckMonitorNow={() => checkIssueMonitorNow.mutate()}
checkingMonitorNow={checkIssueMonitorNow.isPending}
externalReferences={externalObjectsState.markdownReferences}
/>
) : null}
</TabsContent>