Skip to content

TUI: "Copied to clipboard" toast shown even when nothing was copied (OSC52 "attempted" treated as success) #59664

Open

Description

@MostDef2000

Environment

What happens

Mouse-selection copy in the TUI shows the "Copied to clipboard" toast, but the text never reaches the host clipboard. Pasting later returns whatever was in the clipboard before the copy — in our case a stale literal undefined string from an earlier broken copy, which made the failure very confusing to debug.

Root cause

packages/tui/src/clipboard.ts, write() (as of v2.2.0 sources):

const result = await clipboard.writeText(text.replaceAll("\0", ""), {
  destination: "all-available",
  selection: "clipboard",
})
if (result.host.status === "written" || result.terminal.status === "attempted") return

With destination: "all-available", OpenTUI writes via the host backend and emits OSC 52 to the terminal. In this environment the host write never fires (verified: zero Wayland protocol traffic from the TUI on copy attempts — no set_selection / data-control activity, observed in the compositor stub's log), so the only delivery channel is OSC 52.

terminal.status is "attempted" — per OpenTUI's own docs, "OSC 52 attempted does not confirm the terminal accepted it". That status is treated as success: the promise resolves and util/selection.ts shows the toast:

clipboard
  .write(clipboardText)
  .then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
  .catch(toast.error)

VTE-based terminals (GNOME Terminal, XFCE terminal, Tilix, Terminator — the defaults on most Linux desktops) silently ignore OSC 52, so nothing is copied while the user is told it was.

This is not limited to sandbox setups: the same lying toast occurs in any SSH/TUI session where the host clipboard path is unavailable and the local terminal lacks OSC 52 support — a very common combination.

Expected behavior

  • Treat OSC 52 "attempted" as unverified, not success. OpenTUI already has an OSC 52 capability query (isOsc52Supported(), XTGETTCAP-style). When the reported capability is unknown/unsupported and the host write did not succeed, the feedback should reflect that (e.g. "Copied — delivery not confirmed", or an error), not an unqualified success toast.
  • (Secondary) The native Wayland host backend appears to enter a terminal failed/unsupported state after the initial connect and never retries subsequent writes — worth a look for robustness.

Verification

Marker test: selected a unique string that had never been in any clipboard; the "Copied" toast appeared; wl-paste on the host machine returned the previous clipboard content, not the marker. Repeated with the same result.

Activity

  1. opencode-agent commented on Oct 27, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I reproduced this on 2.2.0, and it still happens on the latest dev (96ae220).

    What I did: ran the TUI on Linux with no host clipboard (no DISPLAY, no WAYLAND_DISPLAY, no clipboard helpers), in a virtual terminal that ignores OSC 52, like VTE. Then I drag-selected some text on the home screen. The recording and screenshot below show the "Copied to clipboard" toast, even though nothing received the text.

    In the same environment, OpenTUI's host clipboard returns unsupported, so the only thing sent was the OSC 52 sequence, with status attempted. As you pointed out, write() in packages/tui/src/clipboard.ts counts that as success, so the success toast is shown. The script below reproduces it.

    I did not test your second point, that the Wayland host backend never retries after it first fails, because that needs a compositor. Related: #50208 (the same false toast over SSH + tmux) and #59052 (the same all-available clipboard setting).

    recording.mp4

    Screenshot

    Reproduction script (drive.ts)
    // Repro for #59664: TUI shows "Copied to clipboard" although no clipboard received the text.
    // Sandbox: Linux, no DISPLAY / WAYLAND_DISPLAY, no clipboard helpers -> host write can't succeed.
    // The drive's virtual terminal ignores OSC 52 (like GNOME Terminal / VTE).
    import { Effect } from "effect"
    import { OpenCodeDriver } from "opencode-drive"
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true, pointerOverlay: { leadMs: 180, lingerMs: 700, motionMs: 500, curve: 0.06 } },
      },
      ({ ui }) =>
        Effect.gen(function* () {
          yield* ui.waitFor("Ask anything")
          yield* Effect.sleep("800 millis")
          // Drag-select the "~/files" directory label on the home screen (row 25, cols 13-20)
          yield* ui.mouse({ action: "move", x: 13, y: 25 })
          yield* ui.mouse({ action: "down", x: 13, y: 25, button: "left" })
          yield* ui.mouse({ action: "move", x: 17, y: 25 })
          yield* ui.mouse({ action: "move", x: 20, y: 25 })
          yield* ui.mouse({ action: "up", x: 20, y: 25, button: "left" })
          yield* ui.waitFor("Copied to clipboard")
          yield* ui.screenshot("copied-toast")
          yield* Effect.log(
            `DISPLAY=${JSON.stringify(process.env.DISPLAY ?? "")} WAYLAND_DISPLAY=${JSON.stringify(process.env.WAYLAND_DISPLAY ?? "")} -> no host clipboard exists, yet the toast says "Copied to clipboard"`,
          )
          yield* Effect.sleep("2000 millis")
        }),
    )

    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.

  2. MostDef2000 commented on Oct 28, 2026

    @MostDef2000
    Author

    Follow-up: root cause was on our side — native Wayland copy works; the toast issue stands

    Update after deeper debugging (opencode 2.2.0, same environment). The "host write never fires" observation in the original report was an artifact of our minimal compositor stub, not an OpenTUI/opencode behavior. Two bugs on our side, both fixed:

    1. Blocking backup channel in the compositor stub. The stub ran a blocking urlopen(timeout=6) inline in its single-threaded event loop after each delivery. Any gateway response slower than OpenTUI's 1s host-clipboard timeout (DEFAULT_CLIPBOARD_TIMEOUT_MS = 1_000, host-clipboard.internal.ts) wedged the client backend, whose failed phase is terminal — it never wrote again. The first two copies in a session worked; everything after a slow response died silently.

    2. Missing cancelled event on selection replacement. Our stub did not send the protocol-mandated cancelled to the previous data source when a selection was replaced. OpenTUI's provider pool (wayland.zig: canPublishProvider admits a new provider only while < 2 un-cancelled providers exist per selection type; retireProvider frees a slot only after cancelled AND in-flight transfers completed) then exhausts after exactly two copies — the third copy fails with selectionFailure(.provider) before any network traffic. This part is our protocol non-conformance, not an OpenTUI bug; real compositors send cancelled.

    After fixing the stub, mouse-copy → host clipboard works reliably: 5+ consecutive copies verified in the compositor log, each correctly cancelling the previous, plus interleaved external wl-copy clients.

    What remains valid upstream feedback (the core of this issue):

    • The toast still lies on the failure path. packages/tui/src/clipboard.ts write() resolves success when terminal.status === "attempted" even if host.status is "failed"/skipped — during the wedged period above, every copy showed "Copied to clipboard" while nothing was delivered anywhere (OSC 52 into a VTE terminal goes nowhere). Suggestion: treat a failed host write as a failure (or surface a warning toast) instead of masking it with the terminal "attempted" status.
    • The host backend's failed phase is terminal for the lifetime of the process. A transient compositor stall permanently disables host clipboard writes until the app restarts. Suggestion: allow the backend to recover (retry on the next write, or re-init on display reconnect).

    Happy to provide the stub logs or a minimal non-conformant-compositor reproducer if useful.

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