Skip to content

Revoke the local clipboard offer when the clipboard changes - #63

Merged
dhh merged 2 commits into
omacom:mainfrom
TechLuddite:fix/clipboard-revoke-local-offer
Oct 24, 2026
Merged

dhh merged 2 commits into
omacom:mainfrom
TechLuddite:fix/clipboard-revoke-local-offer

Conversation

@TechLuddite

@TechLuddite TechLuddite commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

When a host's offer takes the local clipboard, the client did not revoke its previous local offer. watch_local returned early for the server's proxy provider, so no new LocalOffer reached any session, and Bridge::on_request serves file requests from the local_files snapshot after checking only the serial. Every connected host, including a background tab, could still request the last locally copied files by index and receive their current contents. #43 left this gap.

The client now revokes the local offer in every session on each clipboard change, before the file listing and before that early return. A revocation clears the offer, its MIME types and files, and aborts transfers being served. The revocation is local to the client, and #68 adds the message that tells the host. Serials keep increasing across revocations. A file open still in flight during a revocation is refused when it completes, and a serve displaced by a reused request id is aborted. Wire messages are unchanged.

Validation:

  • Seven new regressions, each failing with its part of the fix removed: a file request after revocation, a revocation during an in-flight open, an aborted serve, a displaced serve, every session revoked when the host's offer takes the clipboard, serials across revocations, and a normal file offer still served.
  • The ignored clipboard test that scripts/e2e.sh runs (step 5b) now drives watch_local with a real RemoteProvider and checks that both of two sessions are revoked. It passed in all eight e2e runs (nested Hyprland, AMD Radeon 840M). The intermittent step 6 failures in those runs are the e2e race that Wait for the client's clipboard item in e2e, not a MIME type #65 fixes.
  • scripts/check.sh passes.

#68 builds on this one and offers the local clipboard only to the shown session. This conflicts with #80 in the clipboard bridge. I am happy to rebase whichever lands second.

Opus 5.5 high, on behalf of TechLuddite.
Implemented with Claude Opus 5.5 at high effort and reviewed by GPT-6.1 (Codex) at high reasoning effort over three passes, then re-reviewed on 2026-10-19 by Claude, GPT-6.1 and Grok 4.7 at medium effort.

When the active host's offer took the local clipboard, watch_local
returned before sending anything, so every Bridge kept the previous local
offer. File requests are served from the local_files snapshot after a
serial check alone, so any connected host, including a background tab,
could still request the last locally copied files by index and read their
current contents.

watch_local now sends RevokeLocal to every session on each clipboard
change, before the file listing starts and before the early return for
the server's offer. The Bridge clears the current offer, its mime types
and its files, and aborts the transfers it is serving through a new
Transfers::abort_serving. While the server's offer holds the clipboard no
local offer follows, so every request is refused until the next local
copy. The serial counter is kept apart from the current offer, so serials
keep increasing across revocations and a stale request cannot match a
reused one. A file request whose open was in flight during a revocation
is refused once the open completes.

Transfers::serve also aborts a serve it displaces when a peer reuses a
request id. Before, the second serve replaced the first one's abort
handle without aborting it, so the first task kept streaming with no
handle for abort_serving to reach.

The ignored clipboard test that scripts/e2e.sh runs now also offers a
copied file to two sessions through watch_local, puts the server's offer
on the clipboard with set_remote_offer, checks that both sessions are
revoked and get no new local offer, and shows a Bridge refusing a request
for the old offer's file. It clears the clipboard before it ends, so it
leaves no selection behind for the next e2e step.
@greptile-apps

greptile-apps Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no actionable issues were found.

Summary

The client now withdraws its previous clipboard offer from every session whenever the clipboard changes.

  • Clipboard changes revoke the old offer in every session.

TechLuddite explicitly defers notifying the host about revocation to #68. Cancellation on clipboard changes is intentional and documented.

Tests were inspected but not run during this review.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Clipboard changes] --> B[Send RevokeLocal to every session]
    B --> C[Clear saved offer and stop outgoing transfers]
    B --> D{Clipboard holds RemoteProvider?}
    D -->|Yes| E[Do not offer it back]
    D -->|No| F[Build local offer]
    F --> G{Clipboard changed again?}
    G -->|Yes| H[Drop stale result]
    G -->|No| I[Send LocalOffer with next serial]
Loading

Reviews (1) · Last reviewed commit: "Explain clipboard-change cancellation of..." · Reviewed by Greptile

@dhh
dhh merged commit 4095cce into omacom:main Oct 24, 2026
5 checks passed
dhh pushed a commit that referenced this pull request Oct 24, 2026
Every running session received every local clipboard offer, so the host
in a background tab could request each copy without a paste (#29).
Local offers now go only to the session the window shows. This builds
on the revocation from #63: each local change still revokes the offer
in every running session first, and only the offer that follows is
limited to the shown session.

Switching tabs, or showing no session, withdraws the offer from the
session that was shown. It gets RevokeLocal, which refuses further
requests and aborts what it is serving, then an empty offer, which
tells the host to drop the selection it advertised for us. The newly
shown session is then offered the current local selection, since the
clipboard reports only changes. watch_local takes the running sessions
for revocations and the shown session for offers, and asks for both
when it sends, so a slow file listing that finishes after a switch goes
only to the shown session.

Each local offer carries the UI's offer generation, and the bridge
hands it back with every read. When the read starts, the UI checks
that the session is still shown and the generation is still the
latest, and refuses it otherwise. A request the worker accepted before
a switch or a new copy can therefore not read what the clipboard holds
by then. The ClipboardRead handler body is serve_local_read, which the
tests call too.

The offer routing is display-free where it can be (LocalSelection's
changed, begin, finish and generation), so the tests drive the same
routing functions as the app. The ignored e2e clipboard test now has
one shown session and checks that a hidden session is only revoked.

--no-clipboard and offers from the host are unchanged. The README says
which machine receives local copies, and no longer says nothing is sent
until you paste, since a host can ask for the contents without one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants