Skip to content

[Bug]: File chips for files created outside the project root fail to open: "Failed to read workspace file 'docs/X.md' in '<root>'" #8251

Description

@cbangera2

Area: apps/web

Steps to reproduce:

  1. Open a project rooted at a directory that is not the folder the agent works in (e.g. project root ~/Documents, agent working inside ~/Documents/<subdir>).
  2. Ask the agent to create a file relative to its own cwd, e.g. docs/GROUPS_PLAN.md.
  3. In the assistant's reply, click the rendered file chip for that file (message text references it as e.g. docs/GROUPS_PLAN.md:46).
  4. The file preview panel errors instead of opening the file.

Expected behavior:

Clicking the chip opens the file the agent actually created (~/Documents/<subdir>/docs/GROUPS_PLAN.md) in the file preview panel.

Actual behavior:

An error toast and a broken preview panel:

Failed to read workspace file 'docs/GROUPS_PLAN.md' in '/Users/<user>/Documents'.

/Users/<user>/Documents/docs/GROUPS_PLAN.md:46

Impact: Minor bug or occasional failure

Version or commit: 0.0.34-nightly.20260825.1180 (9996038)

Environment: macOS 26.6.2, T3 Code desktop app 0.0.34-nightly.20260825.1180, OpenCode provider

Logs or stack traces:

[projects.readFile] ProjectReadFileError {
  cwd: "/Users/<user>/Documents",
  relativePath: "docs/GROUPS_PLAN.md",
  failure: "operation_failed",
  operation: "realpath-target",
  operationPath: "/Users/<user>/Documents/docs/GROUPS_PLAN.md",
}

Workaround: Open the file via the Files explorer instead of the chip.

Activity

  1. Vantrongs commented on Sep 28, 2026

    @Vantrongs
    Contributor

    Another case with the same error but a different setup. It suggests the cause is wider than the agent's cwd. T3 Code 0.0.43-nightly.20260926.2282, Linux desktop, Claude Code provider.

    1. The thread's workspace is ~/Agent, and the agent's cwd is the same. The agent inspects a second repository with cd ~/Development/other-repo && … in Bash.
    2. Its reply mentions src/lib/content/company.ts, a path relative to that other repository.
    3. T3 renders the inline code as a file chip resolved against the thread's workspace. Hovering shows ~/Agent/src/lib/content/company.ts, and clicking fails with Failed to read workspace file 'src/lib/content/company.ts' in '~/Agent'.

    The same text breaks in any thread. When I quoted the path in another thread (workspace ~/Documents/notes), it became a chip for ~/Documents/notes/src/lib/content/company.ts and failed the same way. The full path ~/Development/other-repo/src/lib/content/company.ts opens correctly. ~/Development/other-repo/... also became a chip, labelled ....

    Resolving against the agent's cwd, as this issue expects, would not fix this case, because here the agent's cwd equals the workspace. Two changes would:

    • Check that the target exists before rendering a chip, and render plain inline code otherwise. resolveInlineCodeFileLinkMeta in ChatMarkdown.tsx decides from the text shape alone (inlineCodeFilePathCandidate). The only lookup is the basename search, and needsWorkspaceBasenameLookup skips it for any path that contains /.
    • Tell the agent the rule. buildRuntimeInstructions (apps/server/src/provider/RuntimeInstructions.ts) mentions absolute paths only for images and videos. Nothing says that paths in inline code become links resolved against the workspace root, so the agent has no reason to write ~/… or absolute paths for files outside it.
  2. jieyuexing commented on Oct 9, 2026

    @jieyuexing

    Note

    Drafted by Claude Opus 5.5 and GPT-6 Astra in T3 Code; reviewed and posted by @jieyuexing.

    I traced the subdirectory case on upstream main b707eeb and reproduced the path selection with the real resolver plus isolated files. This example uses ASCII and includes a directory separator, so neither Unicode classification nor bare-basename lookup is needed to reproduce it.

    Thread workspace: /workspace/repo
    Tool working directory: /workspace/repo/project-a
    File created by the tool: /workspace/repo/project-a/docs/report.md
    Assistant output: report

    ChatView passes the current thread/project worktree path as markdownCwd; MessagesTimeline passes it to ChatMarkdown. resolveMarkdownFileLinkTarget joins docs/report.md to that base, producing /workspace/repo/docs/report.md.

    If that root-level file is absent, the fixture gets ENOENT. If a different file exists there, it reads that file instead. The absolute intended path and the full workspace-relative project-a/docs/report.md both select the intended file.

    Sources:

    • const gitCwd = activeProject
      ? projectScriptCwd({
      project: { cwd: activeProject.workspaceRoot },
      worktreePath: activeThread?.worktreePath ?? null,
      })
      : null;
    • markdownCwd={
      paintOnlyDisplayedTimeline
      ? (heldPaintContext?.markdownCwd ?? undefined)
      : (gitCwd ?? undefined)
      }
    • export function resolveMarkdownFileLinkTarget(
      href: string | undefined,
      cwd?: string,
      baseDir: string | undefined = cwd,
      ): string | null {
      if (!href) return null;
      const target = parseMarkdownFileLink(href);
      if (!target) return null;
      const pathWithPosition = formatFilePathPosition(target);
      if (!isRelativeFilePath(pathWithPosition)) return pathWithPosition;
      if (!baseDir) return null;
      return resolvePathLinkTarget(pathWithPosition, baseDir);
    • export function resolvePathLinkTarget(rawPath: string, cwd: string): string {
      const position = splitFilePathPosition(rawPath);
      const { path } = position;
      let resolvedPath = path;
      if (path.startsWith("~/")) {
      const home = inferHomeFromCwd(cwd);
      if (home) {
      const separator: "/" | "\\" = isWindowsPathStyle(home) ? "\\" : "/";
      resolvedPath = joinPath(home, path.slice(2), separator);
      }
      } else if (!isAbsolutePath(path)) {
      const separator: "/" | "\\" = isWindowsPathStyle(cwd) ? "\\" : "/";
      resolvedPath = joinPath(cwd, path, separator);
      }
      return formatFilePathPosition({ ...position, path: resolvedPath });

    This is resolver/file fixture evidence; I have not captured a new UI/network reproduction. The target environment is not lost in this example: the base directory is the mismatch. A tool's transient cwd is not the thread binding, and using the last command cwd as a universal link base would be ambiguous when a response references multiple directories.

    I found the existing #8254 work and am not proposing a duplicate implementation. Cross-project paths belong with #10553; project-as-Git-subfolder behavior has a separate report in #14492.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions