From 99960383d094b850f0001d3067fa604aa3adf7d6 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:51:40 +0200 Subject: [PATCH] fix: open agent file links in the file viewer (#8098) --- .../t3-markdown-text/src/markdownLinks.ts | 6 +++-- apps/mobile/src/lib/markdownLinks.test.ts | 18 ++++++++++++++ apps/web/src/components/ChatMarkdown.tsx | 3 ++- apps/web/src/markdown-links.test.ts | 24 +++++++++++++++++++ apps/web/src/markdown-links.ts | 7 +++++- 5 files changed, 54 insertions(+), 4 deletions(-) diff --git a/apps/mobile/modules/t3-markdown-text/src/markdownLinks.ts b/apps/mobile/modules/t3-markdown-text/src/markdownLinks.ts index f13891e3ff80b02f24e27cab433ba78248bae585..20637c6ba0f4c4f8f4fade5ff4e34bc66def2310 100644 --- a/apps/mobile/modules/t3-markdown-text/src/markdownLinks.ts +++ b/apps/mobile/modules/t3-markdown-text/src/markdownLinks.ts @@ -3,8 +3,10 @@ import type { MARKDOWN_FILE_ICON_SOURCES } from "./markdownFileIcons.generated"; const WINDOWS_DRIVE_PATH_PATTERN = /^[A-Za-z]:[\\/]/; const WINDOWS_UNC_PATH_PATTERN = /^\\\\/; const RELATIVE_PATH_PREFIX_PATTERN = /^(~\/|\.{1,2}\/)/; -const RELATIVE_FILE_PATH_PATTERN = /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+(?::\d+){0,2}$/; -const RELATIVE_FILE_NAME_PATTERN = /^[A-Za-z0-9._-]+\.[A-Za-z0-9_-]+(?::\d+){0,2}$/; +const RELATIVE_FILE_PATH_PATTERN = + /^(?:[A-Za-z0-9._-]+(?: +[A-Za-z0-9._-]+)*\/)+[A-Za-z0-9._-]+(?: +[A-Za-z0-9._-]+)*(?::\d+){0,2}$/; +const RELATIVE_FILE_NAME_PATTERN = + /^[A-Za-z0-9._-]+(?: +[A-Za-z0-9._-]+)*\.[A-Za-z0-9_-]+(?::\d+){0,2}$/; const POSITION_SUFFIX_PATTERN = /:\d+(?::\d+)?$/; const POSIX_FILE_ROOT_PREFIXES = [ "/Users/", diff --git a/apps/mobile/src/lib/markdownLinks.test.ts b/apps/mobile/src/lib/markdownLinks.test.ts index ff57287b74124c0367193133ef2682e354513126..49a8b46648e1d39585e054375a729b6f070a96b3 100644 --- a/apps/mobile/src/lib/markdownLinks.test.ts +++ b/apps/mobile/src/lib/markdownLinks.test.ts @@ -50,6 +50,24 @@ describe("resolveMarkdownLinkPresentation", () => { }); }); + it.each(["md", "html", "xml"])("recognizes a bare spaced .%s filename", (extension) => { + expect( + resolveMarkdownLinkPresentation(`Updated%20cutover%20checklist.${extension}`), + ).toMatchObject({ + kind: "file", + path: `Updated cutover checklist.${extension}`, + label: `Updated cutover checklist.${extension}`, + }); + }); + + it("recognizes spaced relative paths", () => { + expect(resolveMarkdownLinkPresentation("docs/My%20Folder/checklist.xml")).toMatchObject({ + kind: "file", + path: "docs/My Folder/checklist.xml", + label: "checklist.xml", + }); + }); + it("extracts line fragments from relative file links", () => { expect(resolveMarkdownLinkPresentation("src/main.ts#L18C2")).toMatchObject({ kind: "file", diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index 0482d662aaf0287d6fee4c2ea27fc370c30d55b8..a0a1a932f977ec9da6f9b29d46894a4685d5fccd 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -80,6 +80,7 @@ import { resolveInlineCodeFileLinkMeta, resolveMarkdownFileLinkMeta, rewriteMarkdownFileUriHref, + shouldOpenMarkdownFileLinkInBrowserByDefault, shouldOpenMarkdownFileLinkInEditor, type MarkdownFileLinkMeta, } from "../markdown-links"; @@ -1402,7 +1403,7 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ handleOpenInEditor(); return; } - if (onOpenInBrowser) { + if (onOpenInBrowser && shouldOpenMarkdownFileLinkInBrowserByDefault(iconPath)) { handleOpenInBrowser(); return; } diff --git a/apps/web/src/markdown-links.test.ts b/apps/web/src/markdown-links.test.ts index a1d1094bb8d18d8aceb56666132c342b3ed2eb0a..c7a2bcedcfade16d4a8d7b7faf46d534c3c6a079 100644 --- a/apps/web/src/markdown-links.test.ts +++ b/apps/web/src/markdown-links.test.ts @@ -9,6 +9,7 @@ import { resolveMarkdownFileLinkMeta, resolveMarkdownFileLinkTarget, rewriteMarkdownFileUriHref, + shouldOpenMarkdownFileLinkInBrowserByDefault, shouldOpenMarkdownFileLinkInEditor, } from "./markdown-links"; @@ -69,6 +70,15 @@ describe("shouldOpenMarkdownFileLinkInEditor", () => { }); }); +describe("shouldOpenMarkdownFileLinkInBrowserByDefault", () => { + it("keeps PDFs browser-first while source files open in the file viewer", () => { + expect(shouldOpenMarkdownFileLinkInBrowserByDefault("report.pdf")).toBe(true); + expect(shouldOpenMarkdownFileLinkInBrowserByDefault("report.PDF?download=1")).toBe(true); + expect(shouldOpenMarkdownFileLinkInBrowserByDefault("report.html")).toBe(false); + expect(shouldOpenMarkdownFileLinkInBrowserByDefault("report.xml")).toBe(false); + }); +}); + describe("rewriteMarkdownFileUriHref", () => { it("rewrites file uri hrefs into direct path hrefs", () => { expect(rewriteMarkdownFileUriHref("file:///Users/julius/project/src/main.ts#L42")).toBe( @@ -198,6 +208,20 @@ describe("resolveMarkdownFileLinkTarget", () => { }); }); + it.each(["md", "html", "xml"])( + "resolves a bare spaced .%s filename from the markdown renderer", + (extension) => { + const href = renderMarkdownLinkHref(`[checklist]()`); + + expect(href).toBe(`Updated%20cutover%20checklist.${extension}`); + expect(resolveMarkdownFileLinkMeta(href, "/repo/project")).toMatchObject({ + targetPath: `/repo/project/Updated cutover checklist.${extension}`, + workspaceRelativePath: `Updated cutover checklist.${extension}`, + basename: `Updated cutover checklist.${extension}`, + }); + }, + ); + it("formats tooltip display paths relative to the cwd for slash-prefixed windows paths", () => { expect( resolveMarkdownFileLinkMeta( diff --git a/apps/web/src/markdown-links.ts b/apps/web/src/markdown-links.ts index 6ba2c78e13fbee62484c72d7a0c6c14ee519b3e2..24ecd1906eebc913a3c8f4fa079b83fd5e7441ab 100644 --- a/apps/web/src/markdown-links.ts +++ b/apps/web/src/markdown-links.ts @@ -11,7 +11,8 @@ const EXTERNAL_SCHEME_PATTERN = /^([A-Za-z][A-Za-z0-9+.-]*):(.*)$/; const RELATIVE_PATH_PREFIX_PATTERN = /^(~\/|\.{1,2}\/)/; const RELATIVE_FILE_PATH_PATTERN = /^(?:[A-Za-z0-9._-]+(?: +[A-Za-z0-9._-]+)*\/)+[A-Za-z0-9._-]+(?: +[A-Za-z0-9._-]+)*(?::\d+){0,2}$/; -const RELATIVE_FILE_NAME_PATTERN = /^[A-Za-z0-9._-]+\.[A-Za-z0-9_-]+(?::\d+){0,2}$/; +const RELATIVE_FILE_NAME_PATTERN = + /^[A-Za-z0-9._-]+(?: +[A-Za-z0-9._-]+)*\.[A-Za-z0-9_-]+(?::\d+){0,2}$/; const POSITION_SUFFIX_PATTERN = /:\d+(?::\d+)?$/; const POSITION_ONLY_PATTERN = /^\d+(?::\d+)?$/; // Standard OS and dev-container roots; deliberately excludes app-route-ish @@ -71,6 +72,10 @@ export function shouldOpenMarkdownFileLinkInEditor( return isTerminalLinkActivation(event, platform); } +export function shouldOpenMarkdownFileLinkInBrowserByDefault(path: string): boolean { + return /\.pdf$/i.test(path.split(/[?#]/, 1)[0] ?? ""); +} + function safeDecode(value: string): string { try { return decodeURIComponent(value); -- 2.54.0