Skip to content

tui: image paste fails on WSL2/WSLg with libwayland-client 1.18 — Wayland clipboard backend is rejected, X11 fallback can't serve images #55639

Closed

Description

@zhantss

Description

WSL2 + WSLg (Ubuntu 20.04, Windows 11). Copy an image on the Windows side, press
ctrl+v in the TUI: no attachment. The first time I got a popup whose text started
with "X11 ..." (I didn't copy the full line); after that the same paste does nothing
at all — no error, no attachment.

Why: the 2.0.29 TUI reads the clipboard through OpenTUI's native host clipboard,
which prefers Wayland. Ubuntu 20.04 ships libwayland-client 1.18.0, which has no
wl_proxy_marshal_array_flags, and OpenTUI's loader in
packages/native/src/clipboard/linux.zig declares that symbol as required (only
fields typed optional are tolerated). So the Wayland backend is dropped entirely
and it falls back to X11.
Under WSLg the X11 CLIPBOARD owner is Weston's Xwayland
bridge and it only serves UTF8_STRING, so image/png is refused — that's where the
"X11 clipboard ..." text comes from.

  $ dpkg -l libwayland-client0
  ii  libwayland-client0:amd64 1.18.0-1ubuntu0.1
  $ nm -D /lib/x86_64-linux-gnu/libwayland-client.so.0 | grep marshal_array
  T wl_proxy_marshal_array
  T wl_proxy_marshal_array_constructor
  T wl_proxy_marshal_array_constructor_versioned        # no ..._flags

/mnt/wslg/weston.log while pressing ctrl+v (property OPENTUI_CLIPBOARD is only
used by the X11 backend):

  [14:14:33.594] selection request, CLIPBOARD, target image/png, property OPENTUI_CLIPBOARD
  [14:14:33.594] can only handle UTF8_STRING targets...
  [14:14:33.745] selection request, CLIPBOARD, target image/png, property OPENTUI_CLIPBOARD
  [14:14:33.745] can only handle UTF8_STRING targets...

Workaround that works here: give the process a libwayland-client >= 1.20 (I put a
self-contained copy in a private dir and prepend it to LD_LIBRARY_PATH). After
that image paste works and weston.log shows no more OPENTUI_CLIPBOARD requests.

Suggested fix: make wl_proxy_marshal_array_flags optional in the loader, the way
wl_display_set_max_buffer_size already is in the same struct — that file even
documents the convention. Right now one missing symbol silently downgrades image
paste to a code path that can never work under WSLg.

Related, not duplicates:

Plugins

none

OpenCode version

2.0.29

Steps to reproduce

  1. WSL2 with WSLg, Ubuntu 20.04 (libwayland-client 1.18).
  2. Open opencode v2 in the TUI.
  3. Copy an image on the Windows side.
  4. Press ctrl+v in the prompt.
    -> nothing is attached; on some attempts an error popup starting with "X11 " appears.

Screenshot and/or share link

No response

Operating System

Windows 10 + WSL2 (Ubuntu 20.04, WSLg); kernel 6.18.54.1-microsoft-standard-WSL2

Terminal

Windows Terminal

Activity

  1. opencode-agent commented on Oct 15, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I reproduced this on 2.0.29, and it still happens on the latest dev (4397ee4).

    What I did:

    • Ran a headless sway (Wayland) session.
    • Built libwayland-client.so.0 from wayland 1.18.0. It has no wl_proxy_marshal_array_flags.
    • Put a PNG on the clipboard with wl-copy --type image/png.
    • Pressed ctrl+v in the TUI.

    Results:

    • System (newer) libwayland: the prompt shows [Image 1].
    • libwayland 1.18 via LD_LIBRARY_PATH: nothing is attached and no error is shown, on both 2.0.29 and current dev.

    This fits your diagnosis that the Wayland backend is dropped when that symbol is missing, and your workaround of loading a newer libwayland. I couldn't reproduce the WSLg-specific X11 half (Xwayland serving only UTF8_STRING), because there was no X server in my setup. The loader itself lives in OpenTUI, so the fix will likely need to land there.

    The recording below shows the failing paste with libwayland 1.18 on 2.0.29. The script below reproduces both cases, and its header has the setup steps.

    recording.mp4
    Reproduction script (drive.ts)
    // Repro for anomalyco/opencode#55639: image paste does nothing when the loaded
    // libwayland-client is 1.18 (no wl_proxy_marshal_array_flags).
    //
    // Setup (Debian/Ubuntu):
    //   apt-get install -y sway wl-clipboard imagemagick meson ninja-build libffi-dev libexpat1-dev libxml2-dev
    //   curl -sL https://gitlab.freedesktop.org/wayland/wayland/-/archive/1.18.0/wayland-1.18.0.tar.gz | tar xz
    //   (cd wayland-1.18.0 && meson setup build -Ddocumentation=false && ninja -C build src/libwayland-client.so.0.3.0)
    //   mkdir -p /tmp/lib118 && cp -L wayland-1.18.0/build/src/libwayland-client.so.0 /tmp/lib118/
    //   export XDG_RUNTIME_DIR=/tmp/xdg WAYLAND_DISPLAY=wayland-1; mkdir -m700 -p $XDG_RUNTIME_DIR; unset DISPLAY
    //   WLR_BACKENDS=headless WLR_RENDERER=pixman WLR_LIBINPUT_NO_DEVICES=1 sway -c /dev/null &
    //   convert -size 32x32 xc:red red.png && wl-copy --type image/png < red.png
    // Run:
    //   opencode-drive run drive.ts                    # control: system libwayland -> "[Image 1]" attached
    //   LIBWL=/tmp/lib118 opencode-drive run drive.ts  # libwayland 1.18 -> nothing attached
    // LD_LIBRARY_PATH is passed only to the opencode process.
    import { Effect } from "effect"
    import { OpenCodeDriver } from "opencode-drive"
    
    const label = process.env.LIBWL ? "wayland-1_18" : "system-wayland"
    
    export default OpenCodeDriver.use(
      {
        opencode: {
          ...(process.env.OC_DEV ? { dev: process.env.OC_DEV } : {}),
          ...(process.env.LIBWL ? { env: { LD_LIBRARY_PATH: process.env.LIBWL } } : {}),
        },
        tui: { recording: true, keypressOverlay: true } },
      ({ ui }) =>
        Effect.gen(function* () {
          yield* Effect.sleep("2 seconds")
          yield* ui.screenshot(`${label}-before-paste`)
          yield* ui.press("v", { ctrl: true })
          yield* Effect.sleep("4 seconds")
          yield* ui.screenshot(`${label}-after-paste`)
          const frame = yield* ui.capture()
          yield* Effect.log(`[${label}] screen after ctrl+v:\n` + frame.lines.map((l: any) => l.spans.map((s: any) => s.text).join("").trimEnd()).filter(Boolean).join("\n"))
        }),
    )

    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