# Files changed · PR \#9140 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/9140/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[feat\(files\): open markdown, HTML, and PDF files outside the workspace](https://git.cafe/pingdotgg/t3code/pull/9140.md)

Version: 10

Head commit: c0fa311c308958fa1d6249e57d233e77254d6724

Base commit: ea71a19d4181ce13b0e8f374f45ba9237ee4a9ce

Comparison base: ea71a19d4181ce13b0e8f374f45ba9237ee4a9ce

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx

Status: modified; +9; −2

```diff
@@ -60,6 +60,7 @@
 import { WorkspaceFileWebPreview } from "./WorkspaceFileWebPreview";
 import {
   basename,
+  isAbsolutePath,
   isMarkdownPreviewFile,
   isSvgImagePreviewFile,
   isVideoPreviewFile,
@@ -782,8 +783,14 @@
     );
   }
 
+  const parentDir = relativePath.slice(
+    0,
+    Math.max(relativePath.lastIndexOf("/"), relativePath.lastIndexOf("\\"), 0),
+  );
+  // A host file outside the workspace is not under the project name.
+  const headerSubtitle = isAbsolutePath(relativePath)
+    ? parentDir
+    : [projectName, parentDir].filter(Boolean).join(" · ");
-  const parentDir = relativePath.split("/").slice(0, -1).join("/");
-  const headerSubtitle = [projectName, parentDir].filter(Boolean).join(" · ");
 
   return (
     <ReviewHighlighterProvider>
```

## apps/mobile/src/features/files/filePath.test.ts

Status: modified; +14; −1

```diff
@@ -1,6 +1,19 @@
 import { describe, expect, it } from "vite-plus/test";
 
+import {
+  fileRoutePathSegments,
+  isSvgImagePreviewFile,
+  resolveWorkspaceRelativeFilePath,
+} from "./filePath";
+
+describe("fileRoutePathSegments", () => {
+  it("round-trips workspace-relative and host paths through the route", () => {
+    expect(fileRoutePathSegments("src/main.ts")).toEqual(["src", "main.ts"]);
+    expect(fileRoutePathSegments("/tmp/t3-cleanup/report.md").join("/")).toBe(
+      "/tmp/t3-cleanup/report.md",
+    );
+  });
+});
-import { isSvgImagePreviewFile, resolveWorkspaceRelativeFilePath } from "./filePath";
 
 describe("resolveWorkspaceRelativeFilePath", () => {
   it("keeps normalized workspace-relative paths", () => {
```

## apps/mobile/src/features/files/filePath.ts

Status: modified; +8; −1

```diff
@@ -10,10 +10,17 @@
   return /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
 }
 
+/** A file route holding an absolute path shows a host file outside the workspace. */
+export function isAbsolutePath(value: string): boolean {
-function isAbsolutePath(value: string): boolean {
   return value.startsWith("/") || isWindowsAbsolutePath(value);
 }
 
+/** Route segments that `normalizeRoutePath` joins back into the same path, root included. */
+export function fileRoutePathSegments(path: string): string[] {
+  const segments = path.split("/").filter((segment) => segment.length > 0);
+  return path.startsWith("/") ? ["", ...segments] : segments;
+}
+
 function isWindowsPathStyle(value: string): boolean {
   return isWindowsAbsolutePath(value) || /^[A-Za-z]:\\/.test(value);
 }
```

## apps/mobile/src/features/files/workspaceFileAssetUrl.ts

Status: modified; +10; −4

```diff
@@ -2,7 +2,7 @@
 import { useMemo } from "react";
 
 import { useAssetUrlState, useRefreshAssetUrl } from "../../state/assets";
-import { isVideoPreviewFile, resolveWorkspaceFilePath } from "./filePath";
+import { isAbsolutePath, isVideoPreviewFile, resolveWorkspaceFilePath } from "./filePath";
 
 export function useWorkspaceFileAssetUrlState(props: {
   readonly cwd: string | null;
@@ -18,16 +18,22 @@
     [props.cwd, props.relativePath],
   );
 
+  // Videos stream from an exact-file URL, and so does anything outside the
+  // workspace, where no workspace-scoped URL can exist.
+  const relativePath = props.relativePath;
   const resource = useMemo<AssetResource | null>(
     () =>
+      absolutePath !== null && relativePath !== null && props.threadId !== null
-      absolutePath !== null && props.threadId !== null
         ? {
+            _tag:
+              isVideoPreviewFile(absolutePath) || isAbsolutePath(relativePath)
+                ? "media-file"
+                : "workspace-file",
-            _tag: isVideoPreviewFile(absolutePath) ? "media-file" : "workspace-file",
             threadId: props.threadId,
             path: absolutePath,
           }
         : null,
+    [absolutePath, relativePath, props.threadId],
-    [absolutePath, props.threadId],
   );
   const state = useAssetUrlState(props.environmentId, resource);
   const refresh = useRefreshAssetUrl(props.environmentId, resource);
```

## apps/mobile/src/features/threads/ThreadFeed.tsx

Status: modified; +36; −2

```diff
@@ -166,7 +166,12 @@
 import { useAtomQueryRunner } from "../../state/use-atom-query-runner";
 import { usePreparedConnection } from "../../state/session";
 import * as Option from "effect/Option";
+import {
+  basename,
+  fileRoutePathSegments,
-import { resolveWorkspaceRelativeFilePath } from "../files/filePath";
+  isAbsolutePath,
+  resolveWorkspaceRelativeFilePath,
+} from "../files/filePath";
 import { MARKDOWN_IMAGE_MAX_WIDTH, resolveMarkdownImageDisplaySize } from "./markdownImageSize";
 
 const WIDE_MARKDOWN_BLOCK_OPTIONS = {
@@ -2068,7 +2073,7 @@
           navigation.navigate("ThreadFile", {
             environmentId: String(props.environmentId),
             threadId: String(props.threadId),
+            path: fileRoutePathSegments(relativePath),
-            path: relativePath.split("/").filter((segment) => segment.length > 0),
             ...(presentation.line ? { line: String(presentation.line) } : {}),
           });
           return;
@@ -2090,6 +2095,35 @@
         return;
       }
 
+      // A host file outside the workspace, such as a report an agent wrote to
+      // a temp directory, opens read-only in the file screen.
+      if (presentation.kind === "file" && isAbsolutePath(presentation.path)) {
+        void Haptics.selectionAsync();
+        if (isPdfFile({ name: presentation.path })) {
+          setExpandedFile(
+            (current) =>
+              current ?? {
+                kind: "pdf",
+                name: basename(presentation.path),
+                environmentId: props.environmentId,
+                resource: {
+                  _tag: "media-file",
+                  threadId: props.threadId,
+                  path: presentation.path,
+                },
+              },
+          );
+          return;
+        }
+        navigation.navigate("ThreadFile", {
+          environmentId: String(props.environmentId),
+          threadId: String(props.threadId),
+          path: fileRoutePathSegments(presentation.path),
+          ...(presentation.line ? { line: String(presentation.line) } : {}),
+        });
+        return;
+      }
+
       if (presentation.kind !== "file" && presentation.href) {
         if (/^https?:\/\//i.test(presentation.href) && isPdfFile({ name: presentation.href })) {
           setExpandedFile(
```

## apps/server/src/assets/AssetAccess.test.ts

Status: modified; +6; −4

```diff
@@ -44,7 +44,7 @@
 ).pipe(Layer.provideMerge(NodeServices.layer));
 
 describe("AssetAccess", () => {
-  it.effect("issues exact URLs for images and videos outside the workspace", () =>
+  it.effect("issues exact URLs for media and browser documents outside the workspace", () =>
     Effect.gen(function* () {
       const fs = yield* FileSystem.FileSystem;
       const path = yield* Path.Path;
@@ -54,6 +54,8 @@
         ["screenshot.png", "image/png"],
         ["recording.mp4", "video/mp4"],
         ["recording.webm", "video/webm"],
+        ["report.html", "text/html"],
+        ["report.pdf", "application/pdf"],
       ] as const) {
         const filePath = path.join(outside, name);
         yield* fs.writeFileString(filePath, "media");
@@ -104,12 +106,12 @@
     }).pipe(Effect.provide(testLayer)),
   );
 
-  it.effect("rejects non-media files, disguised targets, and directories", () =>
+  it.effect("rejects non-previewable files, disguised targets, and directories", () =>
     Effect.gen(function* () {
       const fs = yield* FileSystem.FileSystem;
       const path = yield* Path.Path;
       const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-validation-" });
+      for (const name of ["report.md", "secret.txt", "secret.%70ng", "secret.png#private.txt"]) {
-      for (const name of ["report.html", "secret.txt", "secret.%70ng", "secret.png#private.txt"]) {
         const filePath = path.join(root, name);
         yield* fs.writeFileString(filePath, "not media");
         const error = yield* issueAssetUrl({
@@ -118,7 +120,7 @@
         expect(error).toBeInstanceOf(AssetPreviewTypeValidationError);
       }
       const disguisedPath = path.join(root, "disguised.png");
-      yield* fs.symlink(path.join(root, "report.html"), disguisedPath);
+      yield* fs.symlink(path.join(root, "secret.txt"), disguisedPath);
       const disguisedError = yield* issueAssetUrl({
         resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: disguisedPath },
       }).pipe(Effect.flip);
```

## apps/server/src/assets/AssetAccess.ts

Status: modified; +3; −3

```diff
@@ -14,9 +14,9 @@
   AssetWorkspaceRootNormalizationError,
 } from "@t3tools/contracts";
 import {
+  hostPreviewMimeTypeFromExtension,
   isWorkspaceImagePreviewPath,
   isWorkspacePreviewEntryPath,
-  mediaMimeTypeFromExtension,
   WORKSPACE_BROWSER_PREVIEW_EXTENSIONS,
   WORKSPACE_IMAGE_PREVIEW_EXTENSIONS,
 } from "@t3tools/shared/filePreview";
@@ -246,7 +246,7 @@
       if (!canonicalFile) {
         return yield* new AssetWorkspaceAssetNotFoundError({ resource: input.resource });
       }
-      if (mediaMimeTypeFromExtension(path.extname(canonicalFile)) === null) {
+      if (hostPreviewMimeTypeFromExtension(path.extname(canonicalFile)) === null) {
         return yield* new AssetPreviewTypeValidationError({ resource: input.resource });
       }
       const identity = yield* openMediaFile(canonicalFile).pipe(
@@ -598,7 +598,7 @@
       Effect.orElseSucceed(() => null),
     );
     if (canonicalFile !== claims.filePath) return null;
+    const mimeType = hostPreviewMimeTypeFromExtension(path.extname(canonicalFile));
-    const mimeType = mediaMimeTypeFromExtension(path.extname(canonicalFile));
     if (!mimeType) return null;
     const file = yield* openMediaFile(canonicalFile, claims).pipe(
       Effect.tapError((cause) =>
```

## apps/server/src/http.test.ts

Status: modified; +7; −9

```diff
@@ -324,15 +324,13 @@
       "X-Content-Type-Options": "nosniff",
     });
   });
-  it("declares utf-8 for HTML assets so non-ASCII content renders correctly", () => {
-    expect(assetResponseHeaders("/workspace/page.html")).toHaveProperty(
-      "Content-Type",
-      "text/html; charset=utf-8",
-    );
-    expect(assetResponseHeaders("/workspace/PAGE.HTM")).toHaveProperty(
-      "Content-Type",
-      "text/html; charset=utf-8",
-    );
+  it("serves HTML assets as utf-8 inside a sandboxed origin", () => {
+    for (const path of ["/workspace/page.html", "/workspace/PAGE.HTM", "/tmp/report.html"]) {
+      expect(assetResponseHeaders(path)).toMatchObject({
+        "Content-Type": "text/html; charset=utf-8",
+        "Content-Security-Policy": "sandbox allow-scripts allow-forms allow-popups allow-modals",
+      });
+    }
   });
 
   it("downloads uploaded documents without executing their content", () => {
```

## apps/server/src/http.ts

Status: modified; +8; −1

```diff
@@ -51,6 +51,10 @@
 const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "::1", "localhost"]);
 const DESKTOP_RENDERER_ORIGINS = ["t3code://app", "t3code-dev://app"];
 const SVG_CONTENT_SECURITY_POLICY = "default-src 'none'; style-src 'unsafe-inline'; sandbox";
+// HTML previews are agent output, not the app. The sandbox gives the document an
+// opaque origin: scripts run, but same-origin cookies, storage, and API calls are
+// out of reach. Relative sibling assets still load through their signed URLs.
+const HTML_CONTENT_SECURITY_POLICY = "sandbox allow-scripts allow-forms allow-popups allow-modals";
 
 // Types a browser may render as a document if a proxy strips the disposition
 // header. Downloads of these fall back to octet-stream.
@@ -105,7 +109,10 @@
       : inlineVideoMimeType !== undefined && isSafeInlineVideoMimeType(inlineVideoMimeType)
         ? { "Content-Type": inlineVideoMimeType }
         : lowerPath.endsWith(".html") || lowerPath.endsWith(".htm")
+          ? {
-          ? { "Content-Type": "text/html; charset=utf-8" }
+              "Content-Type": "text/html; charset=utf-8",
+              "Content-Security-Policy": HTML_CONTENT_SECURITY_POLICY,
+            }
           : {}),
     ...(!options?.download && lowerPath.endsWith(".svg")
       ? { "Content-Security-Policy": SVG_CONTENT_SECURITY_POLICY }
```

## apps/server/src/workspace/WorkspaceFileSystem.test.ts

Status: modified; +66; −0

```diff
@@ -1,3 +1,6 @@
+// @effect-diagnostics nodeBuiltinImport:off - FileSystem cannot create a FIFO.
+import * as NodeChildProcess from "node:child_process";
+
 import * as NodeServices from "@effect/platform-node/NodeServices";
 import { it, describe, expect } from "@effect/vitest";
 import * as Effect from "effect/Effect";
@@ -73,6 +76,53 @@
       }),
     );
 
+    it.effect("reads host files outside the workspace root by absolute path", () =>
+      Effect.gen(function* () {
+        const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;
+        const path = yield* Path.Path;
+        const cwd = yield* makeTempDir;
+        const outsideDir = yield* makeTempDir;
+        yield* writeTextFile(outsideDir, "cleanup-report.md", "# Report\n");
+        const absolutePath = path.join(outsideDir, "cleanup-report.md");
+
+        const result = yield* workspaceFileSystem.readFile({
+          cwd,
+          relativePath: absolutePath,
+        });
+
+        expect(result).toEqual({
+          relativePath: absolutePath,
+          contents: "# Report\n",
+          byteLength: 9,
+          truncated: false,
+        });
+      }),
+    );
+
+    it.effect("rejects a FIFO without blocking on open", () =>
+      Effect.gen(function* () {
+        const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;
+        const path = yield* Path.Path;
+        const cwd = yield* makeTempDir;
+        const outsideDir = yield* makeTempDir;
+        const fifoPath = path.join(outsideDir, "pipe");
+        yield* Effect.promise(
+          () =>
+            new Promise<void>((resolve, reject) =>
+              NodeChildProcess.execFile("mkfifo", [fifoPath], (error) =>
+                error ? reject(error) : resolve(),
+              ),
+            ),
+        );
+
+        const error = yield* workspaceFileSystem
+          .readFile({ cwd, relativePath: fifoPath })
+          .pipe(Effect.flip);
+
+        expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspacePathNotFileError);
+      }),
+    );
+
     it.effect("rejects reads outside the workspace root", () =>
       Effect.gen(function* () {
         const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;
@@ -212,6 +262,22 @@
       }),
     );
 
+    it.effect("rejects writes by absolute path", () =>
+      Effect.gen(function* () {
+        const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;
+        const path = yield* Path.Path;
+        const cwd = yield* makeTempDir;
+        const outsideDir = yield* makeTempDir;
+        const absolutePath = path.join(outsideDir, "cleanup-report.md");
+
+        const error = yield* workspaceFileSystem
+          .writeFile({ cwd, relativePath: absolutePath, contents: "# Edited\n" })
+          .pipe(Effect.flip);
+
+        expect(error).toBeInstanceOf(WorkspacePaths.WorkspacePathOutsideRootError);
+      }),
+    );
+
     it.effect("invalidates workspace entry search cache after writes", () =>
       Effect.gen(function* () {
         const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
```

## apps/server/src/workspace/WorkspaceFileSystem.ts

Status: modified; +48; −6

```diff
@@ -3,10 +3,13 @@
  * WorkspaceFileSystem - Effect service contract for workspace file mutations.
  *
  * Owns workspace-root-relative file read/write operations and their associated
- * safety checks and cache invalidation hooks.
+ * safety checks and cache invalidation hooks. Reads also accept absolute host
+ * paths so clients can show files an agent left outside the workspace; writes
+ * never leave the root.
  *
  * @module WorkspaceFileSystem
  */
+import * as NodeFS from "node:fs";
 import * as NodeFSP from "node:fs/promises";
 
 import type {
@@ -104,7 +107,10 @@
 export class WorkspaceFileSystem extends Context.Service<
   WorkspaceFileSystem,
   {
+    /**
-    /** Read a UTF-8 text file relative to the workspace root. */
+     * Read a UTF-8 text file relative to the workspace root, or any host file by
+     * absolute path.
+     */
     readonly readFile: (
       input: ProjectReadFileInput,
     ) => Effect.Effect<
@@ -132,9 +138,31 @@
   const workspacePaths = yield* WorkspacePaths.WorkspacePaths;
   const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
 
+  /**
+   * Resolves the file a read targets. Workspace-relative paths must stay inside the
+   * root, symlinks included. An absolute path reads a host file in place, such as a
+   * report an agent wrote to a temp directory; it gets no root check.
+   */
+  const resolveReadTarget = Effect.fn("WorkspaceFileSystem.resolveReadTarget")(function* (
+    input: ProjectReadFileInput,
+  ) {
+    const requestedPath = input.relativePath.trim();
+    if (path.isAbsolute(requestedPath)) {
+      const realTargetPath = yield* Effect.tryPromise({
-  const readFile: WorkspaceFileSystem["Service"]["readFile"] = Effect.fn(
-    "WorkspaceFileSystem.readFile",
-  )(function* (input) {
+        try: () => NodeFSP.realpath(requestedPath),
+        catch: (cause) =>
+          new WorkspaceFileSystemOperationError({
+            workspaceRoot: input.cwd,
+            relativePath: input.relativePath,
+            resolvedPath: requestedPath,
+            operationPath: requestedPath,
+            operation: "realpath-target",
+            cause,
+          }),
+      });
+      return { relativePath: requestedPath, realTargetPath };
+    }
+
     const target = yield* workspacePaths.resolveRelativePathWithinRoot({
       workspaceRoot: input.cwd,
       relativePath: input.relativePath,
@@ -177,10 +205,24 @@
         resolvedPath: realTargetPath,
       });
     }
+    return { relativePath: target.relativePath, realTargetPath };
+  });
 
+  const readFile: WorkspaceFileSystem["Service"]["readFile"] = Effect.fn(
+    "WorkspaceFileSystem.readFile",
+  )(function* (input) {
+    const target = yield* resolveReadTarget(input);
+    const realTargetPath = target.realTargetPath;
+
     return yield* Effect.acquireUseRelease(
       Effect.tryPromise({
+        // Non-blocking so a FIFO cannot hang the open; the stat below rejects
+        // it. Regular files ignore the flag. Windows lacks it.
+        try: () =>
+          NodeFSP.open(
-        try: () => NodeFSP.open(realTargetPath, "r"),
+            realTargetPath,
+            NodeFS.constants.O_RDONLY | (NodeFS.constants.O_NONBLOCK ?? 0),
+          ),
         catch: (cause) =>
           new WorkspaceFileSystemOperationError({
             workspaceRoot: input.cwd,
```

## apps/web/src/browser/openFileInPreview.ts

Status: modified; +9; −1

```diff
@@ -6,6 +6,7 @@
   PreviewSessionSnapshot,
   ScopedThreadRef,
 } from "@t3tools/contracts";
+import { mediaFileReference } from "@t3tools/client-runtime/media-reference";
 import {
   type AtomCommandResult,
   mapAtomCommandResult,
@@ -52,9 +53,14 @@
   });
 }
 
+/**
+ * Opens a browser document in the integrated browser. Inside the workspace the
+ * page may load sibling assets; a file outside it is served on its own.
+ */
 export async function openFileInPreview<AssetError, PreviewError>(input: {
   readonly threadRef: ScopedThreadRef;
   readonly filePath: string;
+  readonly workspaceRoot: string | undefined;
   readonly httpBaseUrl: string;
   readonly createAssetUrl: (input: {
     readonly environmentId: EnvironmentId;
@@ -71,11 +77,13 @@
       ),
     );
   }
+  const insideWorkspace =
+    mediaFileReference(input.filePath, input.workspaceRoot).relativePath !== undefined;
   const assetResult = await input.createAssetUrl({
     environmentId: input.threadRef.environmentId,
     input: {
       resource: {
-        _tag: "workspace-file",
+        _tag: insideWorkspace ? "workspace-file" : "media-file",
         threadId: input.threadRef.threadId,
         path: input.filePath,
       },
```

## apps/web/src/components/ChatMarkdown.tsx

Status: modified; +33; −23

```diff
@@ -164,7 +164,7 @@
 } from "~/lib/openPullRequestLink";
 import { writeTextToClipboard } from "../hooks/useCopyToClipboard";
 import { isPreviewSupportedInRuntime } from "../previewStateStore";
-import { resolvePathLinkTarget } from "../terminal-links";
+import { isAbsolutePath, resolvePathLinkTarget } from "../terminal-links";
 import {
   isBrowserPreviewFile,
   openFileInPreview,
@@ -188,6 +188,8 @@
   parseRawHtml?: boolean;
   /** Append a prompt that invokes a newly created artifact-template skill. */
   onUseArtifactTemplate?: ((template: CodexArtifactTemplate) => void) | undefined;
+  /** Directory that anchors relative links and images; defaults to `cwd`. Set
+      to the file's own directory when rendering a markdown file. */
   imageBaseDir?: string | undefined;
   onImageExpand?: ((preview: ExpandedImagePreview) => void) | undefined;
   extraRemarkPlugins?: NonNullable<ReactMarkdownOptions["remarkPlugins"]>;
@@ -1017,14 +1019,16 @@
   targetPath: string;
   iconPath: string;
   displayPath: string;
+  /** What the files panel opens: workspace-relative inside the workspace, the
+      absolute host path outside it, null when the panel cannot show the file. */
-  workspaceRelativePath: string | null;
+  panelPath: string | null;
   line?: number | undefined;
   label: string;
   copyMarkdown: string;
   theme: "light" | "dark";
   threadRef?: ScopedThreadRef | undefined;
   onOpen?: ((targetPath: string) => Promise<AtomCommandResult<unknown, unknown>>) | undefined;
-  onOpenInPanel: (workspaceRelativePath: string, line: number | undefined) => void;
+  onOpenInPanel: (panelPath: string, line: number | undefined) => void;
   openInEditorMenuLabel: string;
   onOpenInBrowser?: (() => Promise<AtomCommandResult<unknown, unknown>>) | undefined;
   onOpenMedia?: (() => void) | undefined;
@@ -1603,7 +1607,7 @@
   targetPath,
   iconPath,
   displayPath,
+  panelPath,
-  workspaceRelativePath,
   line,
   label,
   copyMarkdown,
@@ -1657,8 +1661,8 @@
   }, [onOpen, targetPath]);
 
   const handleOpenInFilePreview = useCallback(() => {
-    if (threadRef && workspaceRelativePath) {
-      onOpenInPanel(workspaceRelativePath, line);
+    if (threadRef && panelPath) {
+      onOpenInPanel(panelPath, line);
       return;
     }
     if (onOpenMedia) {
@@ -1666,7 +1670,7 @@
       return;
     }
     handleOpenInEditor();
-  }, [handleOpenInEditor, line, onOpenInPanel, onOpenMedia, threadRef, workspaceRelativePath]);
+  }, [handleOpenInEditor, line, onOpenInPanel, onOpenMedia, panelPath, threadRef]);
 
   const handleOpenInBrowser = useCallback(() => {
     if (!onOpenInBrowser) {
@@ -1867,7 +1871,7 @@
 
   const canOpenInEditor = onOpen !== undefined;
   const canOpenInBrowser = onOpenInBrowser !== undefined;
-  const canOpenInPanel = threadRef !== undefined && Boolean(workspaceRelativePath);
+  const canOpenInPanel = threadRef !== undefined && Boolean(panelPath);
   const hasPrimaryAction = hasMarkdownFilePrimaryAction({
     canOpenInEditor,
     canOpenInBrowser,
@@ -1954,7 +1958,7 @@
     previous.targetPath === next.targetPath &&
     previous.iconPath === next.iconPath &&
     previous.displayPath === next.displayPath &&
-    previous.workspaceRelativePath === next.workspaceRelativePath &&
+    previous.panelPath === next.panelPath &&
     previous.line === next.line &&
     previous.label === next.label &&
     previous.copyMarkdown === next.copyMarkdown &&
@@ -2098,24 +2102,24 @@
     for (const href of extractMarkdownLinkHrefs(renderCodexFileCitationsAsMarkdown(text))) {
       const normalizedHref = normalizeMarkdownLinkHrefKey(href);
       if (metaByHref.has(normalizedHref)) continue;
-      const meta = resolveMarkdownFileLinkMeta(normalizedHref, cwd);
+      const meta = resolveMarkdownFileLinkMeta(normalizedHref, cwd, imageBaseDir ?? cwd);
       if (meta) {
         metaByHref.set(normalizedHref, meta);
       }
     }
     return metaByHref;
+  }, [cwd, imageBaseDir, text]);
-  }, [cwd, text]);
   const inlineCodeFileLinkMetaByText = useMemo(() => {
     const metaByText = new Map<string, MarkdownFileLinkMeta>();
     for (const span of extractInlineCodeSpans(text)) {
       if (metaByText.has(span)) continue;
+      const meta = resolveInlineCodeFileLinkMeta(span, cwd, imageBaseDir ?? cwd);
-      const meta = resolveInlineCodeFileLinkMeta(span, cwd);
       if (meta) {
         metaByText.set(span, meta);
       }
     }
     return metaByText;
-  }, [cwd, text]);
+  }, [cwd, imageBaseDir, text]);
   const fileLinkParentSuffixByPath = useMemo(() => {
     const filePaths = [
       ...[...markdownFileLinkMetaByHref.values()].map((meta) => meta.filePath),
@@ -2223,12 +2227,13 @@
       return openFileInPreview({
         threadRef,
         filePath: path,
+        workspaceRoot: cwd,
         httpBaseUrl: preparedConnection.value.httpBaseUrl,
         createAssetUrl,
         openPreview,
       });
     },
+    [createAssetUrl, cwd, openPreview, preparedConnection, threadRef],
-    [createAssetUrl, openPreview, preparedConnection, threadRef],
   );
   const findWorkspaceBasenameMatch = useCallback(
     async (workspaceRelativePath: string) => {
@@ -2251,23 +2256,23 @@
     [cwd, environmentId, searchProjectEntries],
   );
   // A bare filename resolves to the workspace root, which is rarely where the
+  // file is, so ask the index before opening. Absolute host paths open as-is.
-  // file is, so ask the index before opening.
   const openFileInPanel = useCallback(
-    (workspaceRelativePath: string, line: number | undefined) => {
+    (panelPath: string, line: number | undefined) => {
       if (!threadRef) return;
       // Claimed on every open so a synchronous one supersedes a lookup already
       // in flight.
       const isLatestLookup = claimWorkspaceBasenameLookup();
       const openAt = (path: string) =>
         useRightPanelStore.getState().openFile(threadRef, path, line);
+      if (!cwd || !needsWorkspaceBasenameLookup(panelPath)) {
+        openAt(panelPath);
-      if (!cwd || !needsWorkspaceBasenameLookup(workspaceRelativePath)) {
-        openAt(workspaceRelativePath);
         return;
       }
       void (async () => {
-        const match = await findWorkspaceBasenameMatch(workspaceRelativePath);
+        const match = await findWorkspaceBasenameMatch(panelPath);
         if (!isLatestLookup()) return;
-        openAt(match ?? workspaceRelativePath);
+        openAt(match ?? panelPath);
       })();
     },
     [cwd, findWorkspaceBasenameMatch, threadRef],
@@ -2310,6 +2315,11 @@
         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);
 
       return (
         <MarkdownFileLink
@@ -2317,7 +2327,7 @@
           targetPath={fileLinkMeta.targetPath}
           iconPath={fileLinkMeta.filePath}
           displayPath={fileLinkMeta.displayPath}
-          workspaceRelativePath={fileLinkMeta.workspaceRelativePath}
+          panelPath={panelPath}
           line={fileLinkMeta.line}
           label={labelParts.join(" · ")}
           copyMarkdown={copyMarkdown}
@@ -2434,7 +2444,7 @@
         const normalizedHref = href ? normalizeMarkdownLinkHrefKey(href) : "";
         const fileLinkMeta = normalizedHref
           ? (markdownFileLinkMetaByHref.get(normalizedHref) ??
+            resolveMarkdownFileLinkMeta(normalizedHref, cwd, imageBaseDir ?? cwd))
-            resolveMarkdownFileLinkMeta(normalizedHref, cwd))
           : null;
         if (!fileLinkMeta) {
           const faviconHost = resolveExternalWebLinkHost(href);
@@ -2580,7 +2590,7 @@
           const codeText = nodeToPlainText(children);
           const fileLinkMeta =
             inlineCodeFileLinkMetaByText.get(codeText.trim()) ??
+            resolveInlineCodeFileLinkMeta(codeText, cwd, imageBaseDir ?? cwd);
-            resolveInlineCodeFileLinkMeta(codeText, cwd);
           if (fileLinkMeta) {
             return fileLinkChip(
               fileLinkMeta,
```

## apps/web/src/components/RightPanelTabs.tsx

Status: modified; +3; −1

```diff
@@ -498,7 +498,9 @@
     case "files":
       return "Files";
     case "file":
-      return surface.relativePath.slice(surface.relativePath.lastIndexOf("/") + 1);
+      return surface.relativePath.slice(
+        Math.max(surface.relativePath.lastIndexOf("/"), surface.relativePath.lastIndexOf("\\")) + 1,
+      );
     case "terminal":
       return (
         terminalLabelsById.get(surface.activeTerminalId) ??
```

## apps/web/src/components/files/FilePreviewPanel.tsx

Status: modified; +22; −12

```diff
@@ -33,7 +33,7 @@
 import { DIFF_SURFACE_THEME_UNSAFE_CSS, resolveDiffThemeName } from "~/lib/diffRendering";
 import { cn } from "~/lib/utils";
 import { isPreviewSupportedInRuntime } from "~/previewStateStore";
-import { resolvePathLinkTarget } from "~/terminal-links";
+import { isAbsolutePath, resolvePathLinkTarget } from "~/terminal-links";
 import { ScrollArea } from "~/components/ui/scroll-area";
 import { Toggle } from "~/components/ui/toggle";
 import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
@@ -791,6 +791,7 @@
   relativePath,
   contents,
   threadRef,
+  readOnly,
   onPendingChange,
 }: Omit<
   EditableFileSurfaceProps,
@@ -802,6 +803,7 @@
   | "onPostRender"
 > & {
   threadRef: ScopedThreadRef;
+  readOnly: boolean;
 }) {
   const saveCoordinator = useFileSaveCoordinator({
     environmentId,
@@ -817,15 +819,19 @@
         cwd={cwd}
         relativePath={relativePath}
         threadRef={threadRef}
+        onTaskListChange={
-        onTaskListChange={({ markerOffset, checked }) => {
+          readOnly
+            ? undefined
+            : ({ markerOffset, checked }) => {
-          const currentContents =
-            getOptimisticProjectFileQueryData(environmentId, cwd, relativePath)?.contents ??
-            contents;
-          const nextContents = setMarkdownTaskChecked(currentContents, markerOffset, checked);
-          if (nextContents === currentContents) return;
-          setProjectFileQueryData(environmentId, cwd, relativePath, nextContents);
-          saveCoordinator.change(nextContents);
-        }}
+                const currentContents =
+                  getOptimisticProjectFileQueryData(environmentId, cwd, relativePath)?.contents ??
+                  contents;
+                const nextContents = setMarkdownTaskChecked(currentContents, markerOffset, checked);
+                if (nextContents === currentContents) return;
+                setProjectFileQueryData(environmentId, cwd, relativePath, nextContents);
+                saveCoordinator.change(nextContents);
+              }
+        }
       />
     </ScrollArea>
   );
@@ -870,6 +876,8 @@
   const isVideo = relativePath !== null && isWorkspaceVideoPreviewPath(relativePath);
   const isImage = relativePath !== null && !isVideo && isWorkspaceImagePreviewPath(relativePath);
   const isMedia = isImage || isVideo;
+  // A file outside the workspace (an absolute path) is shown, never edited.
+  const isHostFile = relativePath !== null && isAbsolutePath(relativePath);
   const file = useProjectFileQuery(environmentId, cwd, relativePath, !isMedia);
   const [explorerOpen, setExplorerOpen] = useState(initialExplorerOpen);
   // Reading markdown rendered is a preference, not a property of one file. Keeping
@@ -936,6 +944,7 @@
       const result = await openFileInPreview({
         threadRef,
         filePath: absolutePath,
+        workspaceRoot: cwd,
         httpBaseUrl: environmentHttpBaseUrl,
         createAssetUrl,
         openPreview,
@@ -952,7 +961,7 @@
         }),
       );
     })();
-  }, [absolutePath, createAssetUrl, environmentHttpBaseUrl, openPreview, threadRef]);
+  }, [absolutePath, createAssetUrl, cwd, environmentHttpBaseUrl, openPreview, threadRef]);
 
   return (
     <div className="flex min-h-0 flex-1 flex-col overflow-hidden bg-background">
@@ -1128,9 +1137,10 @@
                 relativePath={relativePath}
                 threadRef={threadRef}
                 contents={file.data.contents}
+                readOnly={isHostFile}
                 onPendingChange={onPendingChange}
               />
-            ) : file.data.truncated ? (
+            ) : file.data.truncated || isHostFile ? (
               <Virtualizer
                 key={`${relativePath}:${resolvedTheme}:${file.data.byteLength}`}
                 className="file-preview-virtualizer min-h-0 flex-1 overflow-auto"
```

## apps/web/src/components/files/filePath.test.ts

Status: modified; +19; −1

```diff
@@ -14,10 +14,28 @@
   });
 
   it("normalizes repeated separators", () => {
-    expect(fileBreadcrumbs("workspace", "/src//index.ts").map((crumb) => crumb.label)).toEqual([
+    expect(fileBreadcrumbs("workspace", "src//index.ts").map((crumb) => crumb.label)).toEqual([
       "workspace",
       "src",
       "index.ts",
     ]);
   });
+
+  it("starts host paths outside the workspace at the filesystem root", () => {
+    expect(fileBreadcrumbs("t3code", "/tmp/t3-cleanup/report.md")).toEqual([
+      { label: "tmp", path: "/tmp", kind: "directory" },
+      { label: "t3-cleanup", path: "/tmp/t3-cleanup", kind: "directory" },
+      { label: "report.md", path: "/tmp/t3-cleanup/report.md", kind: "file" },
+    ]);
+    expect(fileBreadcrumbs("t3code", "C:\\Temp\\report.md")).toEqual([
+      { label: "C:", path: "C:", kind: "directory" },
+      { label: "Temp", path: "C:\\Temp", kind: "directory" },
+      { label: "report.md", path: "C:\\Temp\\report.md", kind: "file" },
+    ]);
+    expect(fileBreadcrumbs("t3code", "\\\\server\\share\\report.md").map((c) => c.path)).toEqual([
+      "\\\\server",
+      "\\\\server\\share",
+      "\\\\server\\share\\report.md",
+    ]);
+  });
 });
```

## apps/web/src/components/files/filePath.ts

Status: modified; +14; −3

```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 hostPath = isAbsolutePath(relativePath);
-  const parts = relativePath.split("/").filter(Boolean);
+  const separator = isWindowsAbsolutePath(relativePath) ? "\\" : "/";
+  const parts = relativePath.split(/[\\/]/).filter(Boolean);
+  const root = relativePath.startsWith("\\\\") ? "\\\\" : hostPath && separator === "/" ? "/" : "";
   return [
-    { label: projectName, path: "", kind: "project" },
+    ...(hostPath ? [] : [{ label: projectName, path: "", kind: "project" as const }]),
     ...parts.map((part, index) => ({
       label: part,
-      path: parts.slice(0, index + 1).join("/"),
+      path: root + parts.slice(0, index + 1).join(separator),
       kind: index === parts.length - 1 ? ("file" as const) : ("directory" as const),
     })),
   ];
```

## apps/web/src/markdown-links.test.ts

Status: modified; +16; −0

```diff
@@ -275,6 +275,22 @@
   });
 });
 
+describe("relative links inside a rendered host file", () => {
+  it("anchor to the file's directory while workspace membership follows cwd", () => {
+    const meta = resolveMarkdownFileLinkMeta("appendix.md", "/repo", "/tmp/report");
+    expect(meta).toMatchObject({
+      filePath: "/tmp/report/appendix.md",
+      workspaceRelativePath: null,
+    });
+    const inline = resolveInlineCodeFileLinkMeta("Makefile:12", "/repo", "/tmp/report");
+    expect(inline).toMatchObject({ filePath: "/tmp/report/Makefile", line: 12 });
+    expect(resolveMarkdownFileLinkMeta("src/main.ts", "/repo", "/repo/docs")).toMatchObject({
+      filePath: "/repo/docs/src/main.ts",
+      workspaceRelativePath: "docs/src/main.ts",
+    });
+  });
+});
+
 describe("resolveInlineCodeFileLinkMeta", () => {
   it("links relative paths with file extensions", () => {
     expect(
```

## apps/web/src/markdown-links.ts

Status: modified; +14; −6

```diff
@@ -189,9 +189,15 @@
   return !POSITION_ONLY_PATTERN.test(rest);
 }
 
+/**
+ * `baseDir` anchors relative links; it defaults to the workspace root and is the
+ * file's own directory when rendering a markdown file. `cwd` stays the workspace
+ * root so the result still knows whether the target is inside it.
+ */
 export function resolveMarkdownFileLinkTarget(
   href: string | undefined,
   cwd?: string,
+  baseDir: string | undefined = cwd,
 ): string | null {
   if (!href) return null;
   const rawHref = normalizeMarkdownLinkDestination(href);
@@ -222,8 +228,8 @@
     return pathWithPosition;
   }
 
+  if (!baseDir) return null;
+  return resolvePathLinkTarget(pathWithPosition, baseDir);
-  if (!cwd) return null;
-  return resolvePathLinkTarget(pathWithPosition, cwd);
 }
 
 /**
@@ -235,18 +241,19 @@
 export function resolveInlineCodeFileLinkMeta(
   codeText: string,
   cwd?: string,
+  baseDir: string | undefined = cwd,
 ): MarkdownFileLinkMeta | null {
   const candidate = inlineCodeFilePathCandidate(codeText);
   if (candidate === null) return null;
 
+  const resolved = resolveMarkdownFileLinkMeta(candidate, cwd, baseDir);
-  const resolved = resolveMarkdownFileLinkMeta(candidate, cwd);
   if (resolved) return resolved;
 
   // `Makefile:12` — conventional extensionless names fail the generic
   // markdown-link candidate patterns, but here the :line suffix already
   // marked the span as a file reference.
-  if (cwd && isConventionalFilePosition(candidate)) {
-    return buildFileLinkMetaFromTarget(resolvePathLinkTarget(candidate, cwd), cwd);
+  if (baseDir && isConventionalFilePosition(candidate)) {
+    return buildFileLinkMetaFromTarget(resolvePathLinkTarget(candidate, baseDir), cwd);
   }
   return null;
 }
@@ -276,8 +283,9 @@
 export function resolveMarkdownFileLinkMeta(
   href: string | undefined,
   cwd?: string,
+  baseDir: string | undefined = cwd,
 ): MarkdownFileLinkMeta | null {
-  const targetPath = resolveMarkdownFileLinkTarget(href, cwd);
+  const targetPath = resolveMarkdownFileLinkTarget(href, cwd, baseDir);
   if (!targetPath) return null;
   return buildFileLinkMetaFromTarget(targetPath, cwd);
 }
```

## apps/web/src/rightPanelStore.ts

Status: modified; +1; −0

```diff
@@ -41,6 +41,7 @@
   | {
       id: `file:${string}`;
       kind: "file";
+      /** Workspace-relative, or absolute for a host file outside the workspace. */
       relativePath: string;
       revealLine: number | null;
       revealRequestId: number;
```

[Next page](https://git.cafe/pingdotgg/t3code/pull/9140/diffs?after=ImFwcHMvd2ViL3NyYy9yaWdodFBhbmVsU3RvcmUudHMi&expectedVersion=10&format=markdown)
