Skip to content

tui(v2): /cd tilde expansion uses client home for remote servers #55171

Open

Description

@sghng

Summary

/cd ~ in the TUI expands ~ against the client's home directory before
sending it to the server. When a macOS client attaches to a Linux server, the
tilde becomes /Users/<user>, which does not exist server-side, so the
directory change fails with Location not found: /Users/<user>.

Same class as #47665 (client-side path resolution while attached to a remote
server), but hit through the /cd slash command's tilde expansion rather than
the positional directory argument.

Environment

  • opencode version: 2.0.26 (branch v2)
  • OS: macOS client → Linux server
  • Attach: opencode --server https://<server> (TUI attached to remote opencode serve)
  • Active plugins: none

Reproduction

  1. Start the server on Linux: opencode serve (reachable from the client).
  2. Attach from the macOS client: opencode --server https://<server>.
  3. On the TUI home screen (no session yet), run: /cd ~
  4. Observe failure: "Failed to change directory — Location not found:
    /Users/". The tilde was expanded to the client's home.

Expected Behavior

~ expands according to the server's home (e.g. /home/<user>), since the
server owns the filesystem the location refers to.

Actual Behavior

  • No active session: the client pre-expands ~ → /Users/<user> and sends
    that absolute path in location.get. The server uses the query parameter
    verbatim (no server-side tilde expansion on this endpoint), so Linux tries to
    boot a location at /Users/<user> and fails.
  • With an active session: session.move sends the raw ~ and the server
    expands it correctly with its own home — the move works. But the client still
    records its locally-expanded /Users/... path into directory recents.
  • Autocomplete is also affected: typing /cd ~/ lists the client's home
    (via file.list on the wrong absolute path), producing empty/failed results.

Additional Context

Root cause — packages/tui/src/component/prompt/index.tsx (~line 279), the
session.cd command handler:

const expanded =
  value === "~" ? paths.home : value.startsWith("~/") ? path.join(paths.home, value.slice(2)) : value

paths.home resolves through TuiPathsProvider (packages/tui/src/app.tsx,
home: global.home) to os.homedir() of the client process
(packages/util/src/global.ts). The expanded path is then sent to
location.get, whose server-side middleware (requestRef in
packages/server/src/location.ts) performs no tilde expansion of its own.
The session flow shows the intended split: SessionMove.resolveDestination
(packages/core/src/session/move.ts) expands ~ server-side using the
server's Global.home. location.get lacks the equivalent, and the TUI
expands too early regardless.
Secondary affected sites (same client-home source):

  • directory autocomplete: packages/tui/src/component/prompt/autocomplete.tsx
    passes paths.home into directoryAutocomplete, whose
    directoryAutocompleteSearch (packages/tui/src/prompt/directory-completion.ts)
    maps //x to the client home before calling file.list
  • recents: the session branch of the /cd handler records the
    client-computed path via directoryRecents.touch
    A structural blocker for a clean client-side fix: the server never reports its
    home directory. Location.Info (packages/schema/src/location.ts) carries only
    directory/project, so clients have no authoritative home to expand against.
    Possible directions:
  1. Expand ~ server-side in the location middleware (mirroring
    SessionMove.resolveDestination), letting the TUI send raw input on the
    no-session path — consistent with how session.move already behaves.
  2. Expose the server's home (e.g. a home field on location.get) so TUI
    autocomplete and display can use the correct root client-side.
    Related: cli(v2): positional directory resolved locally even with --server, fails on remote-only paths #47665 (same class, CLI positional directory), Desktop: project picker sends host paths to a remote server, and the resulting failure is silent #44150 (Desktop project
    picker sends host paths), TUI referencing local directory when using attach --dir to connect to a remote server. #5380 / TUI sends absolute local paths causing ENOENT on server side #4686 / attach: @ file mentions use client-local path instead of remote directory #13732 (closed V1 cousins of the
    family).

Activity

  1. opencode-agent commented on Oct 13, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report! I reproduced this on 2.0.26, and it still happens on the latest dev (23e3493).

    To get a client with a different home directory than the server, I started the attached TUI (--server <url>) with a separate home. On the home screen with no session:

    • Typing /cd ~/ makes the directory autocomplete show "Could not search directories. Keep typing to try again."
    • Submitting /cd ~ fails with Failed to change directory — Location not found: <client home>.

    The recording and screenshots below show this, and the script below reproduces it. On dev, the /cd handler still replaces ~ with the client's home before calling location.get, and the server doesn't expand ~ on that endpoint. I only tested the case without a session; I didn't check the recents behaviour you describe for /cd inside an active session.

    Related issues:

    recording.mp4

    Screenshot

    Screenshot

    Reproduction script (drive.ts)
    // Server and TUI get different home directories (as with a macOS client
    // attached to a Linux server). `/cd ~` should resolve against the server home.
    import { Effect } from "effect"
    import { OpenCodeDriver } from "opencode-drive"
    import { chmodSync, writeFileSync } from "node:fs"
    
    // Wrapper that gives the TUI (launched with --server) a different home than
    // the server process. OC_DEV=<checkout> runs the CLI from source instead.
    const wrapper = "/tmp/oc-wrap.sh"
    writeFileSync(wrapper, `#!/usr/bin/env bash
    # Simulates a remote client: when launched as a TUI attached with --server,
    # give the client process a different home directory than the server.
    for a in "$@"; do
      if [ "$a" = "--server" ]; then export OPENCODE_TEST_HOME=/nonexistent-client-home/alice; fi
    done
    if [ -n "$OC_DEV" ]; then
      exec bun --conditions=browser --preload="$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js" "$OC_DEV/packages/cli/src/index.ts" "$@"
    fi
    exec opencode2 "$@"
    `)
    chmodSync(wrapper, 0o755)
    
    export default OpenCodeDriver.use(
      {
        opencode: { command: [wrapper] },
        tui: { recording: true },
      },
      ({ ui }) =>
        Effect.gen(function* () {
          yield* Effect.sleep(1500)
          yield* ui.screenshot("home")
          yield* ui.type("/cd ~/")
          yield* Effect.sleep(1500)
          yield* ui.screenshot("autocomplete")
          // remove the trailing "/" so the command is exactly `/cd ~`
          yield* ui.press("backspace")
          yield* Effect.sleep(800)
          yield* ui.screenshot("typed")
          yield* ui.press("escape")
          yield* ui.enter()
          yield* Effect.sleep(1500)
          yield* ui.screenshot("after-enter")
          yield* ui.waitFor("Location not found", { timeout: 10000 })
          yield* ui.screenshot("error")
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

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