From 57fefdd0987a852a3f068cd3b220cf6a21780f62 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Mon, 24 Aug 2026 14:44:17 +0200 Subject: [PATCH] fix: open agent file links in the file viewer --- .../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 f13891e3ff80..20637c6ba0f4 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 ff57287b7412..49a8b46648e1 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 13024a7516ff..68ba24014d79 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"; @@ -1394,7 +1395,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 a1d1094bb8d1..c7a2bcedcfad 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 6ba2c78e13fb..24ecd1906eeb 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);