diff --git a/ui/src/components/IssueChatThread.tsx b/ui/src/components/IssueChatThread.tsx
index ede1450a2d7..4412153f113 100644
--- a/ui/src/components/IssueChatThread.tsx
+++ b/ui/src/components/IssueChatThread.tsx
@@ -89,6 +89,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { MarkdownBody } from "./MarkdownBody";
+import { WorkspaceFileMarkdownBody } from "./WorkspaceFileMarkdownBody";
import { MarkdownEditor, type MentionOption, type MarkdownEditorRef } from "./MarkdownEditor";
import { Identity } from "./Identity";
import { InlineEntitySelector, type InlineEntityOption } from "./InlineEntitySelector";
@@ -669,15 +670,14 @@ const IssueChatTextPart = memo(function IssueChatTextPart({ text, recessed }: {
return ;
}
return (
-
{text}
-
+
);
});
diff --git a/ui/src/components/MarkdownBody.test.tsx b/ui/src/components/MarkdownBody.test.tsx
index 95098a1b9bd..48b695e8a82 100644
--- a/ui/src/components/MarkdownBody.test.tsx
+++ b/ui/src/components/MarkdownBody.test.tsx
@@ -28,7 +28,6 @@ vi.mock("@/lib/router", () => ({
}: { children: ReactNode; to: string } & React.ComponentProps<"a">) => (
{children}
),
- useLocation: () => ({ pathname: "/", search: "", hash: "" }),
}));
vi.mock("../api/issues", () => ({
@@ -534,22 +533,4 @@ describe("MarkdownBody", () => {
expect(html).toContain('href="/issues/ACME-1"');
});
- it("renders workspace file refs in inline code as workspace file links when enabled", () => {
- const html = renderToStaticMarkup(
-
-
-
- {"Check `ui/src/pages/IssueDetail.tsx:42` please."}
-
-
- ,
- );
- expect(html).toContain('data-workspace-file-link="true"');
- expect(html).toContain("ui/src/pages/IssueDetail.tsx:42");
- });
-
- it("does not create workspace file links when the flag is off", () => {
- const html = renderMarkdown("See `ui/src/pages/IssueDetail.tsx:42` for details.");
- expect(html).not.toContain("data-workspace-file-link");
- });
});
diff --git a/ui/src/components/MarkdownBody.tsx b/ui/src/components/MarkdownBody.tsx
index 100c8ae2761..4d4cb8e7bf7 100644
--- a/ui/src/components/MarkdownBody.tsx
+++ b/ui/src/components/MarkdownBody.tsx
@@ -12,13 +12,7 @@ import { issuesApi } from "../api/issues";
import { queryKeys } from "../lib/queryKeys";
import { parseIssueReferenceFromHref, remarkLinkIssueReferences } from "../lib/issue-reference";
import { remarkSoftBreaks } from "../lib/remark-soft-breaks";
-import {
- parseWorkspaceFileHref,
- remarkWorkspaceFileRefs,
- WORKSPACE_FILE_HREF_PREFIX,
-} from "../lib/remark-workspace-file-refs";
import { StatusIcon } from "./StatusIcon";
-import { WorkspaceFileLink } from "./WorkspaceFileLink";
interface MarkdownBodyProps {
children: string;
@@ -32,12 +26,6 @@ interface MarkdownBodyProps {
wikiLinkRoot?: string;
/** Optional href resolver for wikilinks. Return null to leave a token as plain text. */
resolveWikiLinkHref?: (target: string, label: string) => string | null | undefined;
- /**
- * When true, auto-link workspace file references inside inline code spans
- * (e.g. `ui/src/pages/IssueDetail.tsx:42`). Requires a FileViewerProvider
- * ancestor so that clicking a link opens the viewer.
- */
- linkWorkspaceFileReferences?: boolean;
/** Optional resolver for relative image paths (e.g. within export packages) */
resolveImageSrc?: (src: string) => string | null;
/** Called when a user clicks an inline image */
@@ -172,7 +160,6 @@ function extractMermaidSource(children: ReactNode): string | null {
}
function safeMarkdownUrlTransform(url: string): string {
- if (url.startsWith(WORKSPACE_FILE_HREF_PREFIX)) return url;
return parseMentionChipHref(url) ? url : defaultUrlTransform(url);
}
@@ -580,7 +567,6 @@ export function MarkdownBody({
enableWikiLinks = false,
wikiLinkRoot,
resolveWikiLinkHref,
- linkWorkspaceFileReferences = false,
resolveImageSrc,
onImageClick,
}: MarkdownBodyProps) {
@@ -599,9 +585,6 @@ export function MarkdownBody({
if (linkIssueReferences) {
remarkPlugins.push([remarkLinkIssueReferences, { knownPrefixes }]);
}
- if (linkWorkspaceFileReferences) {
- remarkPlugins.push(remarkWorkspaceFileRefs);
- }
if (softBreaks) {
remarkPlugins.push(remarkSoftBreaks);
}
@@ -653,13 +636,6 @@ export function MarkdownBody({
a: ({ node: _node, href, style: linkStyle, children: linkChildren, ...anchorProps }) => {
const dataProps = anchorProps as Record;
const isWikiLink = dataProps["data-paperclip-wiki-link"] === "true";
- if (linkWorkspaceFileReferences) {
- const workspaceFileRef = parseWorkspaceFileHref(href);
- if (workspaceFileRef) {
- return ;
- }
- }
-
if (isWikiLink && href && !/^[a-z][a-z\d+.-]*:/i.test(href) && !href.startsWith("//")) {
return (
{
+ it("links workspace file refs in inline code to the current issue file viewer", () => {
+ const markdown = linkWorkspaceFileInlineCode(
+ "Check `ui/src/pages/IssueDetail.tsx:42` please.",
+ "/issues/PAP-1",
+ "?tab=chat",
+ "#comment-1",
+ );
+
+ expect(markdown).toContain("[`ui/src/pages/IssueDetail.tsx:42`](");
+ expect(markdown).toContain("/issues/PAP-1?tab=chat&file=ui%2Fsrc%2Fpages%2FIssueDetail.tsx&line=42#comment-1");
+ });
+
+ it("leaves non-file inline code unchanged", () => {
+ expect(linkWorkspaceFileInlineCode("Run `pnpm test`.", "/issues/PAP-1", "", "")).toBe("Run `pnpm test`.");
+ });
+});
diff --git a/ui/src/components/WorkspaceFileMarkdownBody.tsx b/ui/src/components/WorkspaceFileMarkdownBody.tsx
new file mode 100644
index 00000000000..e3764444a77
--- /dev/null
+++ b/ui/src/components/WorkspaceFileMarkdownBody.tsx
@@ -0,0 +1,62 @@
+import type { MouseEvent } from "react";
+import { useMemo } from "react";
+import { useLocation } from "@/lib/router";
+import { readFileViewerStateFromSearch, useFileViewer, writeFileViewerStateToSearch } from "@/context/FileViewerContext";
+import { parseWorkspaceFileRef } from "@/lib/workspace-file-parser";
+import { MarkdownBody } from "./MarkdownBody";
+
+type MarkdownBodyProps = Parameters[0];
+
+const INLINE_CODE_RE = /`([^`\r\n]+)`/g;
+
+function escapeMarkdownLinkLabel(value: string) {
+ return value.replace(/([\\\]])/g, "\\$1");
+}
+
+export function linkWorkspaceFileInlineCode(markdown: string, currentPathname: string, currentSearch: string, currentHash: string) {
+ return markdown.replace(INLINE_CODE_RE, (token, rawCode: string) => {
+ const ref = parseWorkspaceFileRef(rawCode);
+ if (!ref) return token;
+ const nextSearch = writeFileViewerStateToSearch(currentSearch, {
+ path: ref.path,
+ line: ref.line,
+ column: ref.column,
+ workspace: "auto",
+ });
+ const href = `${currentPathname}${nextSearch}${currentHash}`;
+ return `[\`${escapeMarkdownLinkLabel(ref.raw)}\`](${href})`;
+ });
+}
+
+export function WorkspaceFileMarkdownBody({
+ children,
+ ...props
+}: MarkdownBodyProps) {
+ const location = useLocation();
+ const viewer = useFileViewer();
+ const linkedMarkdown = useMemo(
+ () => linkWorkspaceFileInlineCode(children, location.pathname, location.search, location.hash),
+ [children, location.hash, location.pathname, location.search],
+ );
+
+ const handleClick = (event: MouseEvent) => {
+ if (!viewer) return;
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
+ const anchor = (event.target as HTMLElement | null)?.closest("a");
+ if (!anchor) return;
+
+ const url = new URL(anchor.href, window.location.href);
+ if (url.origin !== window.location.origin || url.pathname !== location.pathname) return;
+ const next = readFileViewerStateFromSearch(url.search);
+ if (!next) return;
+
+ event.preventDefault();
+ viewer.open(next);
+ };
+
+ return (
+
+ {linkedMarkdown}
+
+ );
+}