Skip to content

Transcript links open only every other click: re-clicks inside the double-click window are silently dropped #56062

Open

Description

@Magentai

Problem

Clicking a link in the session transcript works about every other attempt. A click that lands on the link does nothing, and clicking again immediately after also fails. Waiting a moment and clicking again works.

Steps to reproduce

  1. Run opencode in a terminal with mouse reporting (e.g. VS Code integrated terminal; mouse capture is on by default).
  2. Get a response containing a link.
  3. Click the link — it opens.
  4. Click the same link again within ~0.5 s — nothing happens. Another immediate click — still nothing.
  5. Wait a moment and click again — it opens.

Cause

clickedLink() in packages/tui/src/ui/link.tsx requires a fresh "cell" selection to treat a release as a click. OpenTUI's nextClickBehavior() (packages/core/src/renderer.ts, CLICK_REPEAT_INTERVAL_MS = 500) classifies a re-click on the same renderable within 500 ms and ±1 cell as a "word" selection, and further rapid clicks as "line" (the counter sticks at 3 while clicking). The gate rejects both, so the link silently does not open — and rapid re-clicks keep the classification pinned at "line", so impatient clicking makes it less likely to work, not more.

Separately, a pointer that crosses a cell boundary between press and release clears selection.isStart, so the release is treated as a drag even when it settles back on the link cell.

Environment

  • opencode v2.0.21 (checked, also present on dev)
  • VS Code integrated terminal on Linux, but any terminal with mouse reporting should behave the same

Activity

  1. opencode-agent commented on Oct 16, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I can confirm this. In the reproduction, the simulated model replies with a link and the script clicks it four times: once, twice more about 250 ms apart, then once after a 1.5 s pause. Only the first and the last click open the link. The two quick re-clicks do nothing, and a multi-click selection seems to be copied instead (a "Copied to clipboard" toast appears). The recording below shows this, and the script below reproduces it.

    One note on the version: v2.0.21 can't open transcript links by clicking at all. That feature first shipped in v2.0.26 (#53903). I reproduced it on v2.0.31 and on the latest dev (83c5abb), where it still happens. clickedLink() in packages/tui/src/ui/link.tsx is unchanged on dev. I only tested repeated clicks in place, not the press–move–release case you also describe.

    Related: #56061 is an open PR for this issue.

    recording.mp4
    Reproduction script (drive.ts)
    // Reproduction for anomalyco/opencode#56062: transcript links only open on every other click.
    // A fake `xdg-open`/`$BROWSER` (see run instructions) appends every opened URL to /tmp/opencode/opened.log.
    //
    //   mkdir -p /workspace/repro/bin /tmp/opencode
    //   printf '#!/bin/sh\necho "$*" >> /tmp/opencode/opened.log\n' > /workspace/repro/bin/xdg-open
    //   chmod +x /workspace/repro/bin/xdg-open
    //   PATH=/workspace/repro/bin:$PATH BROWSER=/workspace/repro/bin/xdg-open opencode-drive run drive.ts
    import { Effect } from "effect"
    import * as fs from "node:fs"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    const LOG = "/tmp/opencode/opened.log"
    const URL_TEXT = "https://example.com/docs"
    const opened = () => (fs.existsSync(LOG) ? fs.readFileSync(LOG, "utf8").split("\n").filter(Boolean).length : 0)
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true, pointerOverlay: true },
      },
      ({ ui, llm }) =>
        Effect.gen(function* () {
          yield* Effect.sync(() => fs.rmSync(LOG, { force: true }))
          yield* llm.queue(Llm.text(`See ${URL_TEXT} for details.`))
          yield* ui.submit("give me a link")
          yield* ui.waitFor("for details.")
          yield* Effect.sleep(1000)
    
          const frame = yield* ui.capture()
          let pos: { x: number; y: number } | undefined
          frame.lines.forEach((line, y) => {
            const text = line.spans.map((s) => s.text).join("")
            const x = text.indexOf(URL_TEXT)
            if (x >= 0 && !text.includes("give me")) pos = { x: x + 5, y }
          })
          if (!pos) return yield* Effect.die("link not found on screen")
          const { x, y } = pos
    
          const click = (label: string) =>
            Effect.gen(function* () {
              const before = opened()
              yield* ui.mouse({ action: "move", x, y })
              yield* ui.mouse({ action: "down", x, y, button: "left" })
              yield* ui.mouse({ action: "up", x, y, button: "left" })
              yield* Effect.sleep(150)
              const result = opened() > before ? "OPENED" : "nothing"
              yield* Effect.log(`${label}: ${result}`)
              return result
            })
    
          const results: string[] = []
          results.push(yield* click("click 1 (fresh)"))
          yield* Effect.sleep(100)
          results.push(yield* click("click 2 (~250ms later)"))
          yield* Effect.sleep(100)
          results.push(yield* click("click 3 (~250ms later)"))
          yield* Effect.sleep(1500)
          results.push(yield* click("click 4 (after 1.5s pause)"))
          yield* ui.screenshot("after-clicks")
          yield* Effect.log(`RESULTS: ${results.join(", ")}`)
          yield* Effect.log(`opened.log:\n${fs.existsSync(LOG) ? fs.readFileSync(LOG, "utf8") : "(empty)"}`)
        }),
    )

    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