mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
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:
1 parent
aa9e782ea3
commit
a2476bb79a
9 files changed
+249
-27
No files matched your search
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user