feat(files): open markdown, HTML, and PDF files outside the workspace#9140

merged3
main
t3code/open-outside-workspace-files
openedSep 2, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 4d ago
6 unresolved threads Β· Jump to file
0 / 26 viewed26 files changedLine totals unavailable

apps/server/src/assets/AssetAccess.ts Β· Earlier revision c1d8c405 Β· right 249

Original diff
@@ -246,7 +246,7 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i
       if (!canonicalFile) {
         return yield* new AssetWorkspaceAssetNotFoundError({ resource: input.resource });
       }
-      if (mediaMimeTypeFromExtension(path.extname(canonicalFile)) === null) {
+      if (hostPreviewMimeTypeFromExtension(path.extname(canonicalFile)) === null) {
M
macroscopeappBotSep 2, 2026

🟠 High assets/AssetAccess.ts:249

hostPreviewMimeTypeFromExtension allows .html and .htm files to be minted as inline asset URLs, so an untrusted host HTML file is served as text/html at the server application's origin and can execute same-origin script in the user's authenticated browser. Keep media-file issuance restricted to media extensions, or apply an HTML isolation policy such as a CSP or sandbox before permitting these files inline.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/assets/AssetAccess.ts around line 249:
2
3`hostPreviewMimeTypeFromExtension` allows `.html` and `.htm` files to be minted as inline asset URLs, so an untrusted host HTML file is served as `text/html` at the server application's origin and can execute same-origin script in the user's authenticated browser. Keep media-file issuance restricted to media extensions, or apply an HTML isolation policy such as a CSP or `sandbox` before permitting these files inline.
J
Julius MarmingeSep 2, 2026

Fixed in c0fa311: inline HTML asset responses now carry Content-Security-Policy: sandbox allow-scripts allow-forms allow-popups allow-modals, which gives the document an opaque origin, so it cannot use the session cookie or reach same-origin APIs or storage. Scripts and sibling assets still work for reports. This applies to workspace HTML too, which had the same exposure before this PR. Test added in http.test.ts.

apps/web/src/components/files/FilePreviewPanel.tsx Β· Earlier revision c1d8c405 Β· right 1140

Original diff
@@ -1128,9 +1137,10 @@ export default function FilePreviewPanel({
                 relativePath={relativePath}
                 threadRef={threadRef}
                 contents={file.data.contents}
+                readOnly={isHostFile}
M
macroscopeappBotSep 2, 2026

🟑 Medium files/FilePreviewPanel.tsx:1140

Absolute Markdown files resolve relative links against the workspace cwd, so ./appendix.md in /tmp/report.md opens /workspace/appendix.md (or a same-named workspace file) instead of /tmp/appendix.md. RenderedMarkdownSurface passes cwd to FileMarkdownPreview, whose ChatMarkdown uses it for ordinary links; derive the link base from the host file’s directory when rendering an absolute path.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/web/src/components/files/FilePreviewPanel.tsx around line 1140:
2
3Absolute Markdown files resolve relative links against the workspace `cwd`, so `./appendix.md` in `/tmp/report.md` opens `/workspace/appendix.md` (or a same-named workspace file) instead of `/tmp/appendix.md`. `RenderedMarkdownSurface` passes `cwd` to `FileMarkdownPreview`, whose `ChatMarkdown` uses it for ordinary links; derive the link base from the host file’s directory when rendering an absolute path.
J
Julius MarmingeSep 2, 2026

Fixed in c0fa311: link resolution takes a base directory that FileMarkdownPreview already computed for images, so relative links in a rendered file anchor to that file's directory while workspace membership still follows cwd. Test added in markdown-links.test.ts.

apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx Β· Earlier revision e52e413f Β· right 783

Original diff
@@ -777,7 +778,10 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
   }
 
   const parentDir = relativePath.split("/").slice(0, -1).join("/");
-  const headerSubtitle = [projectName, parentDir].filter(Boolean).join(" Β· ");
+  // A host file outside the workspace is not under the project name.
+  const headerSubtitle = isAbsolutePath(relativePath)
+    ? parentDir
M
macroscopeappBotSep 2, 2026

🟑 Medium files/ThreadFilesRouteScreen.tsx:783

Windows absolute paths such as C:\temp\report.md render with an empty header subtitle instead of C:\temp. parentDir splits only on /, so the backslash-separated path remains one segment and .slice(0, -1) produces an empty string; compute it with both path separators.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx around line 783:
2
3Windows absolute paths such as `C:\temp\report.md` render with an empty header subtitle instead of `C:\temp`. `parentDir` splits only on `/`, so the backslash-separated path remains one segment and `.slice(0, -1)` produces an empty string; compute it with both path separators.
J
Julius MarmingeSep 2, 2026

Fixed in c1d8c40: parentDir is computed from the last separator of either kind.

apps/server/src/workspace/WorkspaceFileSystem.ts Β· File comment

M
macroscopeappBotSep 2, 2026

🟠 High

https://github.com/pingdotgg/t3code/blob/e52e413fde48a5cfd3ac5926eea284a4e5f66a10/apps/server/src/workspace/WorkspaceFileSystem.ts#L218

Reading an absolute host FIFO such as /tmp/pipe blocks in NodeFSP.open(realTargetPath, "r"), so stat.isFile() is never reached and the request can leave a fiber blocked indefinitely when no writer exists. Stat the resolved target before opening it and reject non-regular files first.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/workspace/WorkspaceFileSystem.ts around line 218:
2
3Reading an absolute host FIFO such as `/tmp/pipe` blocks in `NodeFSP.open(realTargetPath, "r")`, so `stat.isFile()` is never reached and the request can leave a fiber blocked indefinitely when no writer exists. Stat the resolved target before opening it and reject non-regular files first.
J
Julius MarmingeSep 2, 2026

Fixed in c1d8c40: the open now passes O_RDONLY | O_NONBLOCK (same as MediaFile.ts), so a FIFO returns immediately and the existing stat.isFile() check rejects it. Added a mkfifo test.

apps/web/src/components/files/filePath.ts Β· Earlier revision e52e413f Β· right 19–19

Original diff
@@ -1,16 +1,27 @@
+import { isWindowsAbsolutePath } from "@t3tools/shared/path";
+
+import { isAbsolutePath } from "~/terminal-links";
+
 export interface FileBreadcrumb {
   label: string;
   path: string;
   kind: "project" | "directory" | "file";
 }
 
+/**
+ * Crumbs for a workspace-relative path start at the project. An absolute host
+ * path is outside the workspace, so its crumbs start at the filesystem root.
+ */
 export function fileBreadcrumbs(projectName: string, relativePath: string): FileBreadcrumb[] {
-  const parts = relativePath.split("/").filter(Boolean);
+  const hostPath = isAbsolutePath(relativePath);
+  const separator = isWindowsAbsolutePath(relativePath) ? "\\" : "/";
+  const parts = relativePath.split(/[\\/]/).filter(Boolean);
+  const root = hostPath && separator === "/" ? "/" : "";

apps/web/src/components/files/filePath.ts Β· right 19–19

M
macroscopeappBotSep 2, 2026

🟑 Medium files/filePath.ts:19

fileBreadcrumbs drops the UNC prefix from paths such as \\server\share\report.md, so FileBreadcrumb.path and its tooltip show server\share... as a relative path. Preserve the two leading backslashes when constructing the root for UNC paths.

diff
1- const root = hostPath && separator === "/" ? "/" : "";
2+ const root = relativePath.startsWith("\\\\") ? "\\\\" : hostPath && separator === "/" ? "/" : "";
πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/web/src/components/files/filePath.ts around line 19:
2
3`fileBreadcrumbs` drops the UNC prefix from paths such as `\\server\share\report.md`, so `FileBreadcrumb.path` and its tooltip show `server\share...` as a relative path. Preserve the two leading backslashes when constructing the root for UNC paths.
J
Julius MarmingeSep 2, 2026

Fixed in c1d8c40: UNC paths keep their leading backslashes as the root; test added.

apps/web/src/components/ChatMarkdown.tsx Β· Earlier revision e52e413f Β· right 2320

Original diff
@@ -2310,14 +2313,19 @@ function ChatMarkdown({
         mediaMimeTypeFromExtension(
           fileLinkMeta.basename.slice(fileLinkMeta.basename.lastIndexOf(".")),
         ) !== null;
+      // Media outside the workspace keeps the expanded preview; other host
+      // files (a report in a temp dir) open read-only in the files panel.
+      const panelPath =
+        fileLinkMeta.workspaceRelativePath ??
+        (!canPreviewMedia && isAbsolutePath(fileLinkMeta.filePath) ? fileLinkMeta.filePath : null);
C
cursorBotSep 2, 2026

Binary host chips open in panel

Medium Severity

panelPath is set for every non-media absolute host path, so the chip's primary click opens the files panel. readFile then rejects binaries, so host PDFs (when the integrated browser is unavailable), xlsx, and similar agent outputs error instead of falling through to the editor as they did before.

Additional Locations (2)

Reviewed by Cursor Bugbot for commit e52e413fde48a5cfd3ac5926eea284a4e5f66a10. Configure here.

J
Julius MarmingeSep 2, 2026

Intentional, matching existing behavior: a binary file inside the workspace (xlsx, pdf on web) already opens the panel and shows the same 'binary' message today, so host files behave identically rather than worse. The context menu still offers the editor and Reveal, and PDFs render in the panel in the stacked #9143. Dismissing.