TUI sends absolute local paths causing ENOENT on server side #3083

Open
opened 2026-02-16 17:38:32 -05:00 by yindo · 5 comments
Owner

Originally created by @pwdgit on GitHub (Nov 24, 2025).

Originally assigned to: @rekram1-node on GitHub.

Description

HI
When using the OpenCode TUI on Windows connected to a remote opencode serve instance on Linux, file references using @file.py or @folder/file.py are incorrectly resolved on the Windows client instead of the server.

This happens even though the TUI is able to correctly list and browse files FROM the server using @.

So:

  • @ shows files from server → ✔ works
  • Referencing the SAME file in a message → fails (path is expanded locally on Windows)

Error shown on the server

ENOENT: no such file or directory, statx '/C:/Users/myuser/myproject/module/file.py'
    path: "/C:/Users/myuser/myproject/module/file.py",
 syscall: "statx",
   errno: -2,
    code: "ENOENT"

Note: The referenced file does exist on the server at /root/myproject/module/file.py.

Expected behavior

  • When connected to a remote server, @path/to/file.py should be resolved server-side, using the server’s working directory and filesystem.
  • TUI should NOT expand paths locally on Windows (e.g. adding C:\Users\...).

Actual behavior

The TUI:

  1. Lists files from the server correctly using @ (proves remote file access works).
  2. But when referencing a file using @file.py in the prompt, it expands the path locally on Windows, converts it to an absolute Windows path (e.g., C:/Users/...), and sends this invalid path to the Linux server.

Steps to reproduce

  1. On a Linux server:
    opencode serve --hostname 0.0.0.0 --port 4700
    
  2. On a Windows machine:
    opencode attach http://<vps-ip>:4700
    
  3. In TUI:
    • Type @ → browse files on server project directory → ✔ works
    • Select a file like module/file.py
    • Submit the prompt: @module/file.py
  4. Server logs:
    ENOENT: no such file or directory, statx '/C:/Users/.../module/file.py'
    

Environment

  • Server: Ubuntu 22.04, OpenCode CLI 1.0.106
  • Client: Windows 11, OpenCode 1.0.97 & 1.0.106 (pre-release) via Chocolatey.

Proposed Solution / Workaround

(I developed this workaround with assistance from AI to debug the path resolution logic)

The issue lies in packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx. The submit function uses the local environment to resolve paths even when connected remotely.

I found a working fix that:

  1. Dynamic Root Detection: Extracts the remote server's working directory from sync.data.session.
  2. Path Normalization: Strips the local Windows CWD from the file path to force it to be relative.
  3. URL Construction: Rebuilds the file:// URL using the remote root so the Linux server can locate it.

Important Note on the Workaround:
For this dynamic fix to work effectively, the sync.data.session object must be populated with the correct remote directory. I found that currently, this requires a specific workaround: an AGENTS.md file must exist in the project directory and it must include the filePath parameter pointing to itself, for example:

<parameter name="filePath">/root/your_project/AGENTS.md</parameter>

Without this specific parameter present in AGENTS.md, the remote path auto-detection often fails to retrieve the correct server-side root.

Code Changes

In packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx:

1. Add Import

import * as path from "path"

2. Replace the submit function with:

  async function submit() {
    if (props.disabled) return
    if (autocomplete.visible) return
    if (!store.prompt.input) return

    const sessionID = props.sessionID
      ? props.sessionID
      : await (async () => {
          const sessionID = await sdk.client.session.create({}).then((x) => x.data!.id)
          return sessionID
        })()
    const messageID = Identifier.ascending("message")
    let inputText = store.prompt.input

    // -------------------------------------------------------------------------
    // FIX: Dynamic Remote Root Detection
    // Extract the server's working directory from the current or latest session.
    // -------------------------------------------------------------------------
    let REMOTE_ROOT = "" 
    if (sync.data.session && sync.data.session.length > 0) {
      // Try to find the directory for the specific session we are in, 
      // otherwise use the most recent session's directory as the project root.
      const activeSession = sync.data.session.find((s) => s.id === sessionID) || sync.data.session[0]
      if (activeSession?.directory) {
        REMOTE_ROOT = activeSession.directory
      }
    }
    // -------------------------------------------------------------------------

    // Expand pasted text inline before submitting
    const allExtmarks = input.extmarks.getAllForTypeId(promptPartTypeId)
    const sortedExtmarks = allExtmarks.sort((a: { start: number }, b: { start: number }) => b.start - a.start)

    for (const extmark of sortedExtmarks) {
      const partIndex = store.extmarkToPartIndex.get(extmark.id)
      if (partIndex !== undefined) {
        const part = store.prompt.parts[partIndex]
        if (part?.type === "text" && part.text) {
          const before = inputText.slice(0, extmark.start)
          const after = inputText.slice(extmark.end)
          inputText = before + part.text + after
        }
      }
    }

    // FIX: Normalize file paths (Windows Client -> Linux Server)
    const cwd = process.cwd().replace(/\\/g, "/")
    const cwdLower = cwd.toLowerCase()

    const nonTextParts = store.prompt.parts
      .filter((part) => part.type !== "text")
      .map((part) => {
        if (part.type === "file" && part.source?.path) {
          let p = part.source.path.replace(/\\/g, "/")
          const pLower = p.toLowerCase()

          // 1. Strip local Windows CWD to get a clean relative path
          if (pLower.startsWith(cwdLower) || (pLower.startsWith("/") && pLower.includes(cwdLower))) {
             const idx = pLower.indexOf(cwdLower)
             let rel = p.slice(idx + cwdLower.length)
             rel = rel.replace(/^\/+/, "") // Remove leading slashes
             
             // If we found a dirty absolute path, replace it in the message text
             if (rel !== p) {
               const escapeRegExp = (string: string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
               const originalPathRegex = new RegExp(escapeRegExp(part.source.path), 'gi')
               const normalizedPathRegex = new RegExp(escapeRegExp(p), 'gi')

               inputText = inputText.replace(originalPathRegex, rel)
               inputText = inputText.replace(normalizedPathRegex, rel)
               
               p = rel
             }
          } else if (path.isAbsolute(part.source.path)) {
             // Fallback using path.relative logic
             const rel = path.relative(process.cwd(), part.source.path).split(path.sep).join("/")
             const escapeRegExp = (string: string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
             inputText = inputText.replace(new RegExp(escapeRegExp(part.source.path), 'gi'), rel)
             p = rel
          }

          // 2. Construct valid Linux Absolute URL using the Dynamic REMOTE_ROOT
          const cleanRemoteRoot = REMOTE_ROOT.replace(/\/$/, "")
          const cleanRelativePath = p.replace(/^\//, "")
          
          // Combine root + relative path
          let absoluteRemotePath = cleanRelativePath
          if (cleanRemoteRoot) {
             absoluteRemotePath = `${cleanRemoteRoot}/${cleanRelativePath}`
          }
          
          // Use 3 slashes for Linux file protocol if remote path implies root
          const newUrl = `file://${absoluteRemotePath.startsWith('/') ? '' : '/'}${absoluteRemotePath}`

          return {
            ...part,
            url: newUrl, 
            source: {
              ...part.source,
              path: p, // Keep path relative for display
            },
          }
        }
        return part
      })

    if (store.mode === "shell") {
      sdk.client.session.shell({
        path: {
          id: sessionID,
        },
        body: {
          agent: local.agent.current().name,
          model: {
            providerID: local.model.current().providerID,
            modelID: local.model.current().modelID,
          },
          command: inputText,
        },
      })
      setStore("mode", "normal")
    } else if (
      inputText.startsWith("/") &&
      iife(() => {
        const command = inputText.split(" ")[0].slice(1)
        return sync.data.command.some((x) => x.name === command)
      })
    ) {
      let [command, ...args] = inputText.split(" ")
      sdk.client.session.command({
        path: {
          id: sessionID,
        },
        body: {
          command: command.slice(1),
          arguments: args.join(" "),
          agent: local.agent.current().name,
          model: `${local.model.current().providerID}/${local.model.current().modelID}`,
          messageID,
        },
      })
    } else {
      sdk.client.session.prompt({
        path: {
          id: sessionID,
        },
        body: {
          ...local.model.current(),
          messageID,
          agent: local.agent.current().name,
          model: local.model.current(),
          parts: [
            {
              id: Identifier.ascending("part"),
              type: "text",
              text: inputText,
            },
            ...nonTextParts.map((x) => ({
              id: Identifier.ascending("part"),
              ...x,
            })),
          ],
        },
      })
    }
    history.append(store.prompt)
    input.extmarks.clear()
    setStore("prompt", {
      input: "",
      parts: [],
    })
    setStore("extmarkToPartIndex", new Map())
    props.onSubmit?.()

    // temporary hack to make sure the message is sent
    if (!props.sessionID)
      setTimeout(() => {
        route.navigate({
          type: "session",
          sessionID,
        })
      }, 50)
    input.clear()
  }

OpenCode version

1.0.106

Steps to reproduce

  1. sending @file command

Screenshot and/or share link

No response

Operating System

windows as TUI and Linux as server

Terminal

No response

Originally created by @pwdgit on GitHub (Nov 24, 2025). Originally assigned to: @rekram1-node on GitHub. ### Description HI When using the **OpenCode TUI on Windows** connected to a **remote `opencode serve`** instance on Linux, file references using `@file.py` or `@folder/file.py` are incorrectly resolved **on the Windows client** instead of the server. This happens **even though the TUI is able to correctly list and browse files FROM the server using `@`**. So: * `@` **shows files from server** → ✔ works * Referencing the SAME file in a message → ❌ fails (path is expanded locally on Windows) ### **Error shown on the server** ``` ENOENT: no such file or directory, statx '/C:/Users/myuser/myproject/module/file.py' path: "/C:/Users/myuser/myproject/module/file.py", syscall: "statx", errno: -2, code: "ENOENT" ``` *Note: The referenced file **does exist** on the server at `/root/myproject/module/file.py`.* ### **Expected behavior** * When connected to a remote server, `@path/to/file.py` should be resolved **server-side**, using the server’s working directory and filesystem. * TUI should NOT expand paths locally on Windows (e.g. adding `C:\Users\...`). ### **Actual behavior** The TUI: 1. Lists files from the server correctly using `@` (proves remote file access works). 2. But when referencing a file using `@file.py` in the prompt, it expands the path locally on Windows, converts it to an absolute Windows path (e.g., `C:/Users/...`), and sends this invalid path to the Linux server. ### **Steps to reproduce** 1. **On a Linux server:** ```bash opencode serve --hostname 0.0.0.0 --port 4700 ``` 2. **On a Windows machine:** ```powershell opencode attach http://<vps-ip>:4700 ``` 3. **In TUI:** * Type `@` → browse files on server project directory → ✔ works * Select a file like `module/file.py` * Submit the prompt: `@module/file.py` 4. **Server logs:** ``` ENOENT: no such file or directory, statx '/C:/Users/.../module/file.py' ``` ### **Environment** * **Server:** Ubuntu 22.04, OpenCode CLI 1.0.106 * **Client:** Windows 11, OpenCode 1.0.97 & 1.0.106 (pre-release) via Chocolatey. ### **Proposed Solution / Workaround** *(I developed this workaround with assistance from AI to debug the path resolution logic)* The issue lies in `packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx`. The `submit` function uses the local environment to resolve paths even when connected remotely. I found a working fix that: 1. **Dynamic Root Detection:** Extracts the remote server's working directory from `sync.data.session`. 2. **Path Normalization:** Strips the local Windows CWD from the file path to force it to be relative. 3. **URL Construction:** Rebuilds the `file://` URL using the remote root so the Linux server can locate it. **Important Note on the Workaround:** For this dynamic fix to work effectively, the `sync.data.session` object must be populated with the correct remote directory. I found that currently, this requires a specific workaround: an **`AGENTS.md`** file must exist in the project directory and **it must include the `filePath` parameter pointing to itself**, for example: ```xml <parameter name="filePath">/root/your_project/AGENTS.md</parameter> ``` Without this specific parameter present in `AGENTS.md`, the remote path auto-detection often fails to retrieve the correct server-side root. #### **Code Changes** In `packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx`: **1. Add Import** ```typescript import * as path from "path" ``` **2. Replace the `submit` function with:** ```typescript async function submit() { if (props.disabled) return if (autocomplete.visible) return if (!store.prompt.input) return const sessionID = props.sessionID ? props.sessionID : await (async () => { const sessionID = await sdk.client.session.create({}).then((x) => x.data!.id) return sessionID })() const messageID = Identifier.ascending("message") let inputText = store.prompt.input // ------------------------------------------------------------------------- // FIX: Dynamic Remote Root Detection // Extract the server's working directory from the current or latest session. // ------------------------------------------------------------------------- let REMOTE_ROOT = "" if (sync.data.session && sync.data.session.length > 0) { // Try to find the directory for the specific session we are in, // otherwise use the most recent session's directory as the project root. const activeSession = sync.data.session.find((s) => s.id === sessionID) || sync.data.session[0] if (activeSession?.directory) { REMOTE_ROOT = activeSession.directory } } // ------------------------------------------------------------------------- // Expand pasted text inline before submitting const allExtmarks = input.extmarks.getAllForTypeId(promptPartTypeId) const sortedExtmarks = allExtmarks.sort((a: { start: number }, b: { start: number }) => b.start - a.start) for (const extmark of sortedExtmarks) { const partIndex = store.extmarkToPartIndex.get(extmark.id) if (partIndex !== undefined) { const part = store.prompt.parts[partIndex] if (part?.type === "text" && part.text) { const before = inputText.slice(0, extmark.start) const after = inputText.slice(extmark.end) inputText = before + part.text + after } } } // FIX: Normalize file paths (Windows Client -> Linux Server) const cwd = process.cwd().replace(/\\/g, "/") const cwdLower = cwd.toLowerCase() const nonTextParts = store.prompt.parts .filter((part) => part.type !== "text") .map((part) => { if (part.type === "file" && part.source?.path) { let p = part.source.path.replace(/\\/g, "/") const pLower = p.toLowerCase() // 1. Strip local Windows CWD to get a clean relative path if (pLower.startsWith(cwdLower) || (pLower.startsWith("/") && pLower.includes(cwdLower))) { const idx = pLower.indexOf(cwdLower) let rel = p.slice(idx + cwdLower.length) rel = rel.replace(/^\/+/, "") // Remove leading slashes // If we found a dirty absolute path, replace it in the message text if (rel !== p) { const escapeRegExp = (string: string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') const originalPathRegex = new RegExp(escapeRegExp(part.source.path), 'gi') const normalizedPathRegex = new RegExp(escapeRegExp(p), 'gi') inputText = inputText.replace(originalPathRegex, rel) inputText = inputText.replace(normalizedPathRegex, rel) p = rel } } else if (path.isAbsolute(part.source.path)) { // Fallback using path.relative logic const rel = path.relative(process.cwd(), part.source.path).split(path.sep).join("/") const escapeRegExp = (string: string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') inputText = inputText.replace(new RegExp(escapeRegExp(part.source.path), 'gi'), rel) p = rel } // 2. Construct valid Linux Absolute URL using the Dynamic REMOTE_ROOT const cleanRemoteRoot = REMOTE_ROOT.replace(/\/$/, "") const cleanRelativePath = p.replace(/^\//, "") // Combine root + relative path let absoluteRemotePath = cleanRelativePath if (cleanRemoteRoot) { absoluteRemotePath = `${cleanRemoteRoot}/${cleanRelativePath}` } // Use 3 slashes for Linux file protocol if remote path implies root const newUrl = `file://${absoluteRemotePath.startsWith('/') ? '' : '/'}${absoluteRemotePath}` return { ...part, url: newUrl, source: { ...part.source, path: p, // Keep path relative for display }, } } return part }) if (store.mode === "shell") { sdk.client.session.shell({ path: { id: sessionID, }, body: { agent: local.agent.current().name, model: { providerID: local.model.current().providerID, modelID: local.model.current().modelID, }, command: inputText, }, }) setStore("mode", "normal") } else if ( inputText.startsWith("/") && iife(() => { const command = inputText.split(" ")[0].slice(1) return sync.data.command.some((x) => x.name === command) }) ) { let [command, ...args] = inputText.split(" ") sdk.client.session.command({ path: { id: sessionID, }, body: { command: command.slice(1), arguments: args.join(" "), agent: local.agent.current().name, model: `${local.model.current().providerID}/${local.model.current().modelID}`, messageID, }, }) } else { sdk.client.session.prompt({ path: { id: sessionID, }, body: { ...local.model.current(), messageID, agent: local.agent.current().name, model: local.model.current(), parts: [ { id: Identifier.ascending("part"), type: "text", text: inputText, }, ...nonTextParts.map((x) => ({ id: Identifier.ascending("part"), ...x, })), ], }, }) } history.append(store.prompt) input.extmarks.clear() setStore("prompt", { input: "", parts: [], }) setStore("extmarkToPartIndex", new Map()) props.onSubmit?.() // temporary hack to make sure the message is sent if (!props.sessionID) setTimeout(() => { route.navigate({ type: "session", sessionID, }) }, 50) input.clear() } ``` ### OpenCode version 1.0.106 ### Steps to reproduce 1. sending @file command ### Screenshot and/or share link _No response_ ### Operating System windows as TUI and Linux as server ### Terminal _No response_
yindo added the opentuibug labels 2026-02-16 17:38:32 -05:00
Author
Owner

@github-actions[bot] commented on GitHub (Nov 24, 2025):

This issue might be a duplicate of existing issues. Please check:

  • #4522: Missing files when using @ to reference files mid sentence (file reference issues in TUI)
  • #4523: Unable to reference files mid sentence when typing @ after a space (file reference context issues)
  • #4216: Pasting a relative path used to reference it, but now it doesn't (path resolution regression)
  • #4374: File system operations fail on windows with azure provider (Windows path handling issues)
  • #4599: Bash tool executes bash commands with cmd.exe (Windows shell command execution issues)

These issues collectively suggest there are known challenges with file path handling in cross-platform scenarios. Your issue specifically addresses the Windows-to-Linux remote scenario with @file references, which may share root causes with these related issues.

Feel free to ignore if none of these address your specific case.

@github-actions[bot] commented on GitHub (Nov 24, 2025): This issue might be a duplicate of existing issues. Please check: - #4522: Missing files when using @ to reference files mid sentence (file reference issues in TUI) - #4523: Unable to reference files mid sentence when typing @ after a space (file reference context issues) - #4216: Pasting a relative path used to reference it, but now it doesn't (path resolution regression) - #4374: File system operations fail on windows with azure provider (Windows path handling issues) - #4599: Bash tool executes bash commands with cmd.exe (Windows shell command execution issues) These issues collectively suggest there are known challenges with file path handling in cross-platform scenarios. Your issue specifically addresses the Windows-to-Linux remote scenario with @file references, which may share root causes with these related issues. Feel free to ignore if none of these address your specific case.
Author
Owner

@rekram1-node commented on GitHub (Nov 24, 2025):

yeah server should be source of truth this is a bug

@rekram1-node commented on GitHub (Nov 24, 2025): yeah server should be source of truth this is a bug
Author
Owner

@oussamadouhou commented on GitHub (Dec 29, 2025):

PR #6413 ensures the server uses the correct working directory via x-opencode-directory header. This may help with @file reference resolution on remote servers.
Worth testing with Windows client → Linux server setup once merged.

@oussamadouhou commented on GitHub (Dec 29, 2025): PR #6413 ensures the server uses the correct working directory via `x-opencode-directory` header. This may help with `@file` reference resolution on remote servers. Worth testing with Windows client → Linux server setup once merged.
Author
Owner

@himalalps commented on GitHub (Jan 12, 2026):

PR #6413 ensures the server uses the correct working directory via x-opencode-directory header. This may help with @file reference resolution on remote servers. Worth testing with Windows client → Linux server setup once merged.

I am using Mac client -> Linux server, the same bug also occurs.

  • @ shows files from server → ✔ works
  • Referencing the SAME file in a message → fails (path is expanded locally on Mac)

ERROR shown on the server:

ENOENT: no such file or directory, statx '/Users/xxx/code/scripts/run.sh'
    path: "/Users/xxx/code/scripts/run.sh",
 syscall: "statx",
   errno: -2,
    code: "ENOENT"

And the referenced file does exist on the server at /home/xxx/myproject/scripts/run.sh.

@himalalps commented on GitHub (Jan 12, 2026): > PR [#6413](https://github.com/anomalyco/opencode/pull/6413) ensures the server uses the correct working directory via `x-opencode-directory` header. This may help with `@file` reference resolution on remote servers. Worth testing with Windows client → Linux server setup once merged. I am using Mac client -> Linux server, the same bug also occurs. - `@` shows files from server → ✔ works - Referencing the SAME file in a message → ❌ fails (path is expanded locally on Mac) ERROR shown on the server: ``` ENOENT: no such file or directory, statx '/Users/xxx/code/scripts/run.sh' path: "/Users/xxx/code/scripts/run.sh", syscall: "statx", errno: -2, code: "ENOENT" ``` And the referenced file does exist on the server at `/home/xxx/myproject/scripts/run.sh`.
Author
Owner

@joao-coli commented on GitHub (Jan 14, 2026):

I'm having this issue even trying to use the linux server exclusively.

-> SSH into the sever
-> opencode server --hostname 0.0.0.0 --port XYZ
-> In another tab or tmux session/window, opencode attach localhost:XYZ

Then, all attempts to @ result in the same errors as others have described (and the message simply dissappears into thin air).

If I run only opencode --hostname 0.0.0.0 --port XYZ and use it straight from there, no errors occur. But any attached session prevent me from linking files

@joao-coli commented on GitHub (Jan 14, 2026): I'm having this issue even trying to use the linux server exclusively. -> SSH into the sever -> `opencode server --hostname 0.0.0.0 --port XYZ` -> In another tab or tmux session/window, `opencode attach localhost:XYZ` Then, all attempts to `@` result in the same errors as others have described (and the message simply dissappears into thin air). If I run only `opencode --hostname 0.0.0.0 --port XYZ` and use it straight from there, no errors occur. But any attached session prevent me from linking files
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#3083