Skip to content

desktop: path links skip directories and paths with spaces #55863

Description

@q1023884985

Description

In the desktop app, a local path in a chat message becomes a clickable link only if it passes an existence check, but the check matches files only. Directories and paths containing spaces are never linkified, and a manually written markdown link to a directory renders as a link but fails on click with a "File not found" error toast.

Verified in desktop 2.0.29 (renderer bundles inside app.asar):

  1. The file service exists() implementation lists the parent directory and accepts only entries where type === "file" and the basename matches. Directories come back as type: "directory" from GET /api/fs/list, so a folder can never be linkified.
  2. The inline-code path classifier rejects any candidate containing whitespace, so a file path like C:\Program Files\app\file.txt is never linkified either — while the markdown-link route (file:// URL) has no whitespace check and links the same path fine.
  3. Clicking a link to a directory runs the file-read flow, which returns 404 FileNotFoundError for directories, producing the error toast "Failed to load file / File not found: ".

The main process already exposes shell.openPath and showItemInFolder (wired to the Review panel file tree), so opening/revealing a directory is feasible — it just isn't wired into the chat-link open flow.

Suggested fixes:

  • Allow whitespace in the inline-code path classifier (the span boundary is already delimited by the markdown backticks).
  • Accept type === "directory" in exists() (folder icons already exist in the tree renderer).
  • In the link open flow, fall back to a parent-directory listing on read 404: open/reveal directories via the existing bridge instead of toasting an error.

Plugins

none

OpenCode version

2.0.29 (desktop app; standalone CLI 2.0.15)

Steps to reproduce

  1. Desktop app, any session.
  2. Have the agent output an existing folder path in inline code, e.g. C:\Users\foo\folder.
  3. Observe: it renders as plain code, not a link.
  4. Have it output an existing file path containing spaces, e.g. C:\Program Files\app\file.txt — also not a link.
  5. Have it output the folder as a markdown link instead: [folder](file:///C:/Users/foo/folder) — it renders as a link.
  6. Click it → error toast: "Failed to load file / File not found: folder".

Screenshot and/or share link

n/a — repro is deterministic text; the error toast reads "Failed to load file / File not found: folder".

Operating System

Windows 11 (10.0.26200)

Terminal

n/a (desktop app)

Activity

  1. opencode-agent commented on Oct 15, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I reproduced all three problems on 2.0.29, and they still happen on the latest dev (a448d5b).

    I couldn't run the desktop app here, so I ran the code it relies on against a real opencode serve:

    • Folders: /api/fs/list returns a folder as type: "directory". The desktop's exists() check accepts only type === "file", so it answers false and the folder never becomes a link.
    • Paths with spaces: the inline-code classifier returns nothing for /…/my folder/file.txt and C:\Program Files\app\file.txt. That's because of its whitespace check, even though the existence check would find the file.
    • Clicking a folder link: GET /api/fs/read/<folder> returns 404 FileNotFoundError ("File not found: "), which is the text of the toast you saw.

    The script below reproduces this. Related issues about desktop path links: #50390, #51872, #54540, #51516, and #37891, which asks for clickable paths that open or reveal.

    Reproduction script (repro.ts)
    // Reproduces anomalyco/opencode#55863 without the desktop shell.
    // Usage: start `opencode serve --port 4567` (prints a password), then
    //   OC_PASSWORD=<password> OC_REPO=/workspace/repo bun /workspace/repro/repro.ts
    // It (1) runs the desktop's inline-code classifier on paths, and (2) replays the
    // desktop `exists()` check (packages/app/src/workspaces/files/model.tsx) and the
    // click-to-open read (/api/fs/read) against a real server, for a directory and a
    // file whose path contains a space.
    import { mkdirSync, writeFileSync } from "node:fs"
    import { join, dirname, basename } from "node:path"
    
    const repo = process.env.OC_REPO ?? "/workspace/repo"
    const { inlineCodeKind } = await import(`${repo}/packages/session-ui/src/components/markdown-inline-code-kind.ts`)
    
    const base = process.env.OC_URL ?? "http://127.0.0.1:4567"
    const auth = "Basic " + Buffer.from(`opencode:${process.env.OC_PASSWORD ?? ""}`).toString("base64")
    const proj = "/workspace/repro/proj"
    mkdirSync(join(proj, "sub"), { recursive: true })
    mkdirSync(join(proj, "my folder"), { recursive: true })
    writeFileSync(join(proj, "my folder/file.txt"), "hi\n")
    
    const q = (extra: Record<string, string>) =>
      new URLSearchParams({ "location[directory]": proj, ...extra }).toString()
    
    // Same logic as the desktop `exists()`: list parent, accept only type === "file".
    async function exists(p: string) {
      const res = await fetch(`${base}/api/fs/list?${q({ path: dirname(p) })}`, { headers: { authorization: auth } })
      if (!res.ok) return { ok: false, status: res.status }
      const body: any = await res.json()
      const entries: any[] = body.data ?? body
      const match = entries.find((e) => basename(e.path) === basename(p))
      return { listedType: match?.type, existsResult: !!match && match.type === "file" }
    }
    
    async function read(p: string) {
      const res = await fetch(`${base}/api/fs/read/${encodeURI(p)}?${q({})}`, { headers: { authorization: auth } })
      return { status: res.status, body: (await res.text()).slice(0, 200) }
    }
    
    for (const p of [join(proj, "sub"), join(proj, "my folder/file.txt"), "C:\\Program Files\\app\\file.txt", join(proj, "sub/a.txt")]) {
      console.log(JSON.stringify(p), "inlineCodeKind =", inlineCodeKind(p))
    }
    console.log("directory  exists():", await exists(join(proj, "sub")))
    console.log("spaced file exists():", await exists(join(proj, "my folder/file.txt")))
    console.log("read(directory) on click:", await read("sub"))

    Run it: bun repro.ts

  2. Finn763 commented on Oct 19, 2026

    @Finn763
    Contributor

    I'd like to work on this — the file-link existence check only accepts files and the inline-code classifier rejects whitespace, so directories and paths with spaces never linkify. I'll fix both and cover them with tests.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions